Compare commits
No commits in common. "b479dbe519e821a813ec190e97f7da931a61b4af" and "211fa195cb53b12ba8bf02d8f88d8c62152712d8" have entirely different histories.
b479dbe519
...
211fa195cb
58
README.md
58
README.md
@ -1,54 +1,16 @@
|
||||
# Swing Account (摇摇记账)
|
||||
# swing_account
|
||||
|
||||
一个简单易用的记账应用,支持摇一摇快速记账。
|
||||
A new Flutter project.
|
||||
|
||||
## 功能特点
|
||||
## Getting Started
|
||||
|
||||
### 记账功能
|
||||
- 支持收入/支出记录
|
||||
- 摇一摇快速记账
|
||||
- 记录分类管理
|
||||
- 备注和图片附件
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
### 数据统计
|
||||
- 月度收支统计
|
||||
* 月支出总额
|
||||
* 月收入总额
|
||||
* 月结余计算
|
||||
* 金额显示/隐藏切换
|
||||
- 最近7日支出统计
|
||||
* 柱状图显示
|
||||
* 自动计算比例
|
||||
* 支持金额缩写(k/w)
|
||||
- 按日期分组的记录列表
|
||||
* 日期分组显示
|
||||
* 每日收支统计
|
||||
* 支持编辑和删除
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
### 界面交互
|
||||
- 月份筛选
|
||||
- 记录详情查看
|
||||
- 支持记录编辑/删除
|
||||
- 响应式布局设计
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 核心功能
|
||||
- 使用 SQLite 本地数据存储
|
||||
- 传感器集成实现摇一摇
|
||||
- 支持图片附件存储
|
||||
- 精确的数值计算
|
||||
|
||||
### 依赖包
|
||||
- sensors_plus: 传感器支持
|
||||
- vibration: 震动反馈
|
||||
- sqflite: 数据库
|
||||
- decimal: 精确计算
|
||||
- uuid: 唯一标识
|
||||
- path: 文件路径处理
|
||||
|
||||
## 开发环境
|
||||
- Flutter SDK: >=3.2.5 <4.0.0
|
||||
- Dart SDK: >=3.2.5 <4.0.0
|
||||
|
||||
## 项目结构
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
||||
@ -46,7 +46,7 @@ android {
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<application
|
||||
android:label="摇晃记账"
|
||||
android:label="swing_account"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=file:///C:/Users/24811/.gradle/wrapper/dists/gradle-7.5-all.zip
|
||||
distributionUrl=file:///D:/Gradle/.gradle/wrapper/dists/gradle-7.5-all.zip
|
||||
@ -1,200 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/record.dart';
|
||||
|
||||
/// 分类配置
|
||||
class CategoryConfig {
|
||||
static const Map<String, Map<String, dynamic>> expenseCategories = {
|
||||
'food': {
|
||||
'name': '餐饮',
|
||||
'iconKey': 'food',
|
||||
'sortOrder': 0,
|
||||
},
|
||||
'shopping': {
|
||||
'name': '购物',
|
||||
'iconKey': 'shopping',
|
||||
'sortOrder': 1,
|
||||
},
|
||||
'transport': {
|
||||
'name': '交通',
|
||||
'iconKey': 'transport',
|
||||
'sortOrder': 2,
|
||||
},
|
||||
'entertainment': {
|
||||
'name': '娱乐',
|
||||
'iconKey': 'entertainment',
|
||||
'sortOrder': 3,
|
||||
},
|
||||
'housing': {
|
||||
'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,
|
||||
},
|
||||
};
|
||||
/// 预设支出分类
|
||||
final List<Category> expenseCategories = [
|
||||
Category(
|
||||
id: 'food',
|
||||
name: '餐饮',
|
||||
icon: Icons.restaurant,
|
||||
type: RecordType.expense,
|
||||
),
|
||||
Category(
|
||||
id: 'shopping',
|
||||
name: '购物',
|
||||
icon: Icons.shopping_bag,
|
||||
type: RecordType.expense,
|
||||
),
|
||||
Category(
|
||||
id: 'transport',
|
||||
name: '交通',
|
||||
icon: Icons.directions_bus,
|
||||
type: RecordType.expense,
|
||||
),
|
||||
Category(
|
||||
id: 'entertainment',
|
||||
name: '娱乐',
|
||||
icon: Icons.sports_esports,
|
||||
type: RecordType.expense,
|
||||
),
|
||||
// 可以继续添加更多分类...
|
||||
];
|
||||
|
||||
static const Map<String, Map<String, dynamic>> incomeCategories = {
|
||||
'salary': {
|
||||
'name': '工资',
|
||||
'iconKey': 'salary',
|
||||
'sortOrder': 0,
|
||||
},
|
||||
'bonus': {
|
||||
'name': '奖金',
|
||||
'iconKey': 'bonus',
|
||||
'sortOrder': 1,
|
||||
},
|
||||
'investment': {
|
||||
'name': '投资',
|
||||
'iconKey': 'investment',
|
||||
'sortOrder': 2,
|
||||
},
|
||||
'partTime': {
|
||||
'name': '兼职',
|
||||
'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();
|
||||
}
|
||||
}
|
||||
/// 预设收入分类
|
||||
final List<Category> incomeCategories = [
|
||||
Category(
|
||||
id: 'salary',
|
||||
name: '工资',
|
||||
icon: Icons.account_balance_wallet,
|
||||
type: RecordType.income,
|
||||
),
|
||||
Category(
|
||||
id: 'bonus',
|
||||
name: '奖金',
|
||||
icon: Icons.card_giftcard,
|
||||
type: RecordType.income,
|
||||
),
|
||||
Category(
|
||||
id: 'investment',
|
||||
name: '投资',
|
||||
icon: Icons.trending_up,
|
||||
type: RecordType.income,
|
||||
),
|
||||
// 可以继续添加更多分类...
|
||||
];
|
||||
@ -1,49 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
21
lib/data/mock_data.dart
Normal file
21
lib/data/mock_data.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import '../models/record.dart';
|
||||
|
||||
final List<Record> mockRecords = [
|
||||
Record(
|
||||
id: '1',
|
||||
type: RecordType.expense,
|
||||
categoryId: 'food',
|
||||
note: '午餐',
|
||||
amount: 25.0,
|
||||
createTime: DateTime.now().subtract(const Duration(hours: 2)),
|
||||
),
|
||||
Record(
|
||||
id: '2',
|
||||
type: RecordType.income,
|
||||
categoryId: 'salary',
|
||||
note: '工资',
|
||||
amount: 5000.0,
|
||||
createTime: DateTime.now().subtract(const Duration(days: 1)),
|
||||
),
|
||||
// 可以添加更多模拟数据...
|
||||
];
|
||||
@ -1,7 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import './pages/record_page.dart';
|
||||
import './pages/statistics_page.dart';
|
||||
import './pages/profile_page.dart';
|
||||
import './pages/home_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@ -12,63 +10,14 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 移除debug标签
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: '记账本',
|
||||
theme: ThemeData(
|
||||
primaryColor: const Color(0xFF9FE2BF),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF9FE2BF),
|
||||
),
|
||||
),
|
||||
home: const MainPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
const MainPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MainPage> createState() => _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
// 页面列表
|
||||
final List<Widget> _pages = [
|
||||
const RecordPage(),
|
||||
const StatisticsPage(),
|
||||
const ProfilePage(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: _pages[_currentIndex],
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.account_balance_wallet),
|
||||
label: '记账',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.insert_chart),
|
||||
label: '统计',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.person),
|
||||
label: '我的',
|
||||
),
|
||||
],
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: const HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../data/icons.dart';
|
||||
|
||||
/// 记账类型枚举
|
||||
enum RecordType {
|
||||
@ -11,38 +10,17 @@ enum RecordType {
|
||||
class Category {
|
||||
final String id;
|
||||
final String name;
|
||||
final String iconKey; // 改为使用图标key
|
||||
final IconData icon;
|
||||
final RecordType type;
|
||||
final int sortOrder; // 添加排序字段
|
||||
final String? parentId; // 为未来二级分类预留
|
||||
|
||||
const Category({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.iconKey,
|
||||
required this.icon,
|
||||
required this.type,
|
||||
required this.sortOrder,
|
||||
this.parentId,
|
||||
});
|
||||
|
||||
// 获取图标
|
||||
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'],
|
||||
);
|
||||
}
|
||||
|
||||
/// 记账记录模型
|
||||
|
||||
@ -1,532 +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';
|
||||
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,11 +3,10 @@ import 'package:sensors_plus/sensors_plus.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'dart:async';
|
||||
import '../models/record.dart';
|
||||
import 'edit_page.dart';
|
||||
import './record_page.dart';
|
||||
import '../widgets/record_list.dart';
|
||||
import '../data/mock_data.dart'; // 暂时使用模拟数据
|
||||
import '../services/database_service.dart';
|
||||
import '../widgets/weekly_spending_chart.dart';
|
||||
import '../widgets/monthly_summary_card.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
@ -19,42 +18,10 @@ class HomePage extends StatefulWidget {
|
||||
class _HomePageState extends State<HomePage> {
|
||||
StreamSubscription? _accelerometerSubscription;
|
||||
DateTime? _lastShakeTime;
|
||||
List<double> _recentXValues = []; // 存储原始x轴加速度值(不取绝对值)
|
||||
int _shakeCount = 0;
|
||||
bool _isNavigating = false;
|
||||
final _dbService = DatabaseService();
|
||||
List<Record> records = [];
|
||||
DateTime _selectedMonth = DateTime.now(); // 添加选中月份状态
|
||||
bool _isAmountVisible = true; // 添加可视状态
|
||||
|
||||
// 定义摇晃检测的常量
|
||||
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() {
|
||||
@ -63,28 +30,9 @@ class _HomePageState extends State<HomePage> {
|
||||
_loadRecords();
|
||||
}
|
||||
|
||||
/// 重置摇晃检测状态
|
||||
void _resetShakeDetection() {
|
||||
_lastShakeTime = null;
|
||||
_recentXValues.clear();
|
||||
_isNavigating = false;
|
||||
|
||||
// 重新初始化传感器监听
|
||||
_accelerometerSubscription?.cancel();
|
||||
_initShakeDetection();
|
||||
}
|
||||
|
||||
/// 加载记录
|
||||
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,
|
||||
);
|
||||
|
||||
final loadedRecords = await _dbService.getAllRecords();
|
||||
setState(() {
|
||||
records = loadedRecords;
|
||||
records.sort((a, b) => b.createTime.compareTo(a.createTime));
|
||||
@ -93,48 +41,30 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
/// 初始化摇晃检测
|
||||
void _initShakeDetection() {
|
||||
_accelerometerSubscription = accelerometerEvents.listen(
|
||||
(event) {
|
||||
if (_isNavigating) return; // 如果正在导航,忽略摇晃检测
|
||||
|
||||
_accelerometerSubscription = accelerometerEvents.listen((event) {
|
||||
// 主要检测左右摇晃(x轴)
|
||||
if (event.x.abs() > 25) {
|
||||
final now = DateTime.now();
|
||||
|
||||
// 添加新的x轴值(保留正负号)
|
||||
_recentXValues.add(event.x);
|
||||
if (_recentXValues.length > _sampleSize) {
|
||||
_recentXValues.removeAt(0);
|
||||
}
|
||||
|
||||
// 检查是否处于冷却期
|
||||
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) {
|
||||
if (_lastShakeTime == null) {
|
||||
_lastShakeTime = now;
|
||||
_shakeCount = 1;
|
||||
} else {
|
||||
// 缩短有效时间窗口,要求更快的摇晃
|
||||
if (now.difference(_lastShakeTime!) < const Duration(milliseconds: 500)) {
|
||||
_shakeCount++;
|
||||
if (_shakeCount >= 2) {
|
||||
_navigateToRecordPageWithVibration();
|
||||
_shakeCount = 0;
|
||||
_lastShakeTime = null;
|
||||
_recentXValues.clear();
|
||||
} else {
|
||||
_lastShakeTime = now;
|
||||
_recentXValues.clear();
|
||||
}
|
||||
} else {
|
||||
// 重置计数
|
||||
_shakeCount = 1;
|
||||
}
|
||||
_lastShakeTime = now;
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('Accelerometer error: $error');
|
||||
_resetShakeDetection(); // 发生错误时重置检测
|
||||
},
|
||||
cancelOnError: false, // 错误时不取消订阅,而是重置
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 带震动的记账页面导航
|
||||
@ -152,11 +82,10 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
/// 导航到记账页面
|
||||
void _navigateToRecordPage([Record? record]) {
|
||||
_isNavigating = true;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RecordEditPage(
|
||||
builder: (context) => RecordPage(
|
||||
record: record,
|
||||
onSave: (newRecord) async {
|
||||
if (record != null) {
|
||||
@ -170,8 +99,7 @@ class _HomePageState extends State<HomePage> {
|
||||
maintainState: false,
|
||||
),
|
||||
).then((_) {
|
||||
// 返回时重置检测状态
|
||||
_resetShakeDetection();
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -181,82 +109,20 @@ class _HomePageState extends State<HomePage> {
|
||||
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(); // 重新加载数据
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
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: RecordList(
|
||||
records: records,
|
||||
onRecordTap: _navigateToRecordPage,
|
||||
onRecordDelete: _deleteRecord,
|
||||
),
|
||||
),
|
||||
],
|
||||
body: RecordList(
|
||||
records: records,
|
||||
onRecordTap: _navigateToRecordPage,
|
||||
onRecordDelete: _deleteRecord,
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _navigateToRecordPage(),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfilePage extends StatelessWidget {
|
||||
const ProfilePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('我的'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('个人中心开发中...'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,269 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sensors_plus/sensors_plus.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'dart:async';
|
||||
import '../models/record.dart';
|
||||
import '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';
|
||||
import '../data/categories.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class RecordPage extends StatefulWidget {
|
||||
const RecordPage({Key? key}) : super(key: key);
|
||||
final Record? record;
|
||||
final Function(Record) onSave; // 添加保存回调
|
||||
|
||||
const RecordPage({
|
||||
Key? key,
|
||||
this.record,
|
||||
required this.onSave,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RecordPage> createState() => _RecordPageState();
|
||||
}
|
||||
|
||||
class _RecordPageState extends State<RecordPage> {
|
||||
StreamSubscription? _accelerometerSubscription;
|
||||
DateTime? _lastShakeTime;
|
||||
final List<double> _recentXValues = []; // 存储原始x轴加速度值(不取绝对值)
|
||||
bool _isNavigating = false;
|
||||
final _dbService = DatabaseService();
|
||||
List<Record> records = [];
|
||||
DateTime _selectedMonth = DateTime.now(); // 添加选中月份状态
|
||||
bool _isAmountVisible = true; // 添加可视状态
|
||||
|
||||
// 定义摇晃检测的常量
|
||||
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;
|
||||
}
|
||||
class _RecordPageState extends State<RecordPage> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
String? _selectedCategoryId;
|
||||
String _note = '';
|
||||
double _amount = 0.0;
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
final _uuid = const Uuid();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initShakeDetection();
|
||||
_loadRecords();
|
||||
}
|
||||
// 如果是编辑模式,初始化现有记录的数据
|
||||
if (widget.record != null) {
|
||||
_selectedCategoryId = widget.record!.categoryId;
|
||||
_note = widget.record!.note ?? '';
|
||||
_amount = widget.record!.amount;
|
||||
_selectedDate = widget.record!.createTime;
|
||||
}
|
||||
|
||||
/// 重置摇晃检测状态
|
||||
void _resetShakeDetection() {
|
||||
_lastShakeTime = null;
|
||||
_recentXValues.clear();
|
||||
_isNavigating = false;
|
||||
|
||||
// 重新初始化传感器监听
|
||||
_accelerometerSubscription?.cancel();
|
||||
_initShakeDetection();
|
||||
}
|
||||
|
||||
/// 加载记录
|
||||
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,
|
||||
_tabController = TabController(
|
||||
length: 2,
|
||||
vsync: this,
|
||||
initialIndex: widget.record?.type == RecordType.income ? 1 : 0,
|
||||
);
|
||||
_tabController.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
/// 标签切换监听
|
||||
void _onTabChanged() {
|
||||
setState(() {
|
||||
records = loadedRecords;
|
||||
records.sort((a, b) => b.createTime.compareTo(a.createTime));
|
||||
_selectedCategoryId = null; // 切换类型时重置选中的分类
|
||||
});
|
||||
}
|
||||
|
||||
/// 初始化摇晃检测
|
||||
void _initShakeDetection() {
|
||||
_accelerometerSubscription = accelerometerEvents.listen(
|
||||
(event) {
|
||||
if (_isNavigating) return; // 如果正在导航,忽略摇晃检测
|
||||
/// 获取当前记录类型
|
||||
RecordType get _currentType =>
|
||||
_tabController.index == 0 ? RecordType.expense : RecordType.income;
|
||||
|
||||
final now = DateTime.now();
|
||||
/// 获取当前分类列表
|
||||
List<Category> get _currentCategories =>
|
||||
_currentType == RecordType.expense ? expenseCategories : incomeCategories;
|
||||
|
||||
// 添加新的x轴值(保留正负号)
|
||||
_recentXValues.add(event.x);
|
||||
if (_recentXValues.length > _sampleSize) {
|
||||
_recentXValues.removeAt(0);
|
||||
}
|
||||
|
||||
// 检查是否处于冷却期
|
||||
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);
|
||||
/// 保存记录
|
||||
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;
|
||||
}
|
||||
|
||||
_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,
|
||||
final record = Record(
|
||||
id: widget.record?.id ?? _uuid.v4(),
|
||||
type: _currentType,
|
||||
categoryId: _selectedCategoryId!,
|
||||
note: _note.isEmpty ? null : _note,
|
||||
amount: _amount,
|
||||
createTime: _selectedDate,
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedMonth = DateTime(picked.year, picked.month);
|
||||
});
|
||||
_loadRecords(); // 重新加载数据
|
||||
}
|
||||
widget.onSave(record);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('记账本'),
|
||||
title: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: '支出'),
|
||||
Tab(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: RecordList(
|
||||
records: records,
|
||||
onRecordTap: _navigateToRecordPage,
|
||||
onRecordDelete: _deleteRecord,
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
),
|
||||
itemCount: _currentCategories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = _currentCategories[index];
|
||||
final isSelected = category.id == _selectedCategoryId;
|
||||
return _buildCategoryItem(category, isSelected);
|
||||
},
|
||||
),
|
||||
),
|
||||
// 底部编辑区域
|
||||
_buildBottomEditor(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建分类项
|
||||
Widget _buildCategoryItem(Category category, bool isSelected) {
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _selectedCategoryId = category.id),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Theme.of(context).primaryColor.withOpacity(0.1) : null,
|
||||
border: Border.all(
|
||||
color: isSelected ? Theme.of(context).primaryColor : Colors.grey,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
category.icon,
|
||||
color: isSelected ? Theme.of(context).primaryColor : Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
category.name,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Theme.of(context).primaryColor : Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建底部编辑区域
|
||||
Widget _buildBottomEditor() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 备注和金额
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '添加备注',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onChanged: (value) => setState(() => _note = value),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'¥${_amount.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 配置按钮
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: 实现账户选择
|
||||
},
|
||||
icon: const Icon(Icons.account_balance_wallet),
|
||||
label: const Text('默认账户'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedDate,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (date != null) {
|
||||
setState(() => _selectedDate = date);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.calendar_today),
|
||||
label: Text(_selectedDate == DateTime.now()
|
||||
? '今天'
|
||||
: '${_selectedDate.month}月${_selectedDate.day}日'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: 实现图片选择
|
||||
},
|
||||
icon: const Icon(Icons.photo),
|
||||
label: const Text('图片'),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 数字键盘
|
||||
_buildNumberKeyboard(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建数字键盘
|
||||
Widget _buildNumberKeyboard() {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
crossAxisCount: 4,
|
||||
childAspectRatio: 2,
|
||||
children: [
|
||||
_buildKeyboardButton('7'),
|
||||
_buildKeyboardButton('8'),
|
||||
_buildKeyboardButton('9'),
|
||||
_buildKeyboardButton('删除', isFunction: true),
|
||||
_buildKeyboardButton('4'),
|
||||
_buildKeyboardButton('5'),
|
||||
_buildKeyboardButton('6'),
|
||||
_buildKeyboardButton('+'),
|
||||
_buildKeyboardButton('1'),
|
||||
_buildKeyboardButton('2'),
|
||||
_buildKeyboardButton('3'),
|
||||
_buildKeyboardButton('-'),
|
||||
_buildKeyboardButton('again'),
|
||||
_buildKeyboardButton('0'),
|
||||
_buildKeyboardButton('.'),
|
||||
_buildKeyboardButton('保存', isFunction: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建键盘按钮
|
||||
Widget _buildKeyboardButton(String text, {bool isFunction = false}) {
|
||||
return TextButton(
|
||||
onPressed: () => _onKeyboardButtonPressed(text),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: isFunction ? Theme.of(context).primaryColor : Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 处理键盘按钮点击
|
||||
void _onKeyboardButtonPressed(String value) {
|
||||
switch (value) {
|
||||
case '删除':
|
||||
setState(() {
|
||||
final amountStr = _amount.toStringAsFixed(2);
|
||||
if (amountStr.length > 1) {
|
||||
_amount = double.parse(amountStr.substring(0, amountStr.length - 1));
|
||||
} else {
|
||||
_amount = 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case '保存':
|
||||
_saveRecord();
|
||||
break;
|
||||
case 'again':
|
||||
// TODO: 实现again功能
|
||||
break;
|
||||
case '+':
|
||||
case '-':
|
||||
// TODO: 实现加减功能
|
||||
break;
|
||||
case '.':
|
||||
// TODO: 实现小数点功能
|
||||
break;
|
||||
default:
|
||||
if (_amount == 0) {
|
||||
setState(() => _amount = double.parse(value));
|
||||
} else {
|
||||
setState(() => _amount = double.parse('$_amount$value'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_accelerometerSubscription?.cancel();
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,153 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/record.dart';
|
||||
import '../services/database_service.dart';
|
||||
import '../widgets/statistics/view_selector.dart';
|
||||
import '../widgets/statistics/monthly_overview.dart';
|
||||
import '../widgets/statistics/daily_chart.dart';
|
||||
import '../widgets/statistics/category_chart.dart';
|
||||
import '../widgets/statistics/daily_report.dart';
|
||||
import '../widgets/statistics/monthly_chart.dart';
|
||||
import '../widgets/statistics/monthly_report.dart';
|
||||
|
||||
class StatisticsPage extends StatefulWidget {
|
||||
const StatisticsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatisticsPage> createState() => _StatisticsPageState();
|
||||
}
|
||||
|
||||
class _StatisticsPageState extends State<StatisticsPage> {
|
||||
final _dbService = DatabaseService();
|
||||
bool _isMonthView = true; // true为月视图,false为年视图
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
List<Record> _records = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadRecords();
|
||||
}
|
||||
|
||||
/// 加载记录数据
|
||||
Future<void> _loadRecords() async {
|
||||
DateTime startDate;
|
||||
DateTime endDate;
|
||||
|
||||
if (_isMonthView) {
|
||||
startDate = DateTime(_selectedDate.year, _selectedDate.month, 1);
|
||||
endDate = DateTime(_selectedDate.year, _selectedDate.month + 1, 0, 23, 59, 59);
|
||||
} else {
|
||||
startDate = DateTime(_selectedDate.year, 1, 1);
|
||||
endDate = DateTime(_selectedDate.year, 12, 31, 23, 59, 59);
|
||||
}
|
||||
|
||||
final records = await _dbService.getRecordsByDateRange(startDate, endDate);
|
||||
setState(() => _records = records);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('统计'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 视图选择器
|
||||
ViewSelector(
|
||||
isMonthView: _isMonthView,
|
||||
selectedDate: _selectedDate,
|
||||
onViewChanged: (isMonth) {
|
||||
setState(() {
|
||||
_isMonthView = isMonth;
|
||||
_loadRecords();
|
||||
});
|
||||
},
|
||||
onDateChanged: (date) {
|
||||
setState(() {
|
||||
_selectedDate = date;
|
||||
_loadRecords();
|
||||
});
|
||||
},
|
||||
),
|
||||
// 数据详情
|
||||
Expanded(
|
||||
child: _isMonthView ? _buildMonthView() : _buildYearView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建月视图
|
||||
Widget _buildMonthView() {
|
||||
if (_records.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('暂无数据'),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 月度总览
|
||||
MonthlyOverview(records: _records),
|
||||
const SizedBox(height: 16),
|
||||
// 每日统计图表
|
||||
if (_hasValidRecords) ...[
|
||||
DailyChart(records: _records),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// 分类统计图表
|
||||
if (_hasValidRecords) ...[
|
||||
CategoryChart(records: _records),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// 日报表
|
||||
if (_hasValidRecords)
|
||||
DailyReport(records: _records),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建年视图
|
||||
Widget _buildYearView() {
|
||||
if (_records.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('暂无数据'),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 年度总览
|
||||
MonthlyOverview(
|
||||
records: _records,
|
||||
isYearView: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 每月统计图表
|
||||
if (_hasValidRecords) ...[
|
||||
MonthlyChart(records: _records),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// 分类统计图表
|
||||
if (_hasValidRecords) ...[
|
||||
CategoryChart(records: _records),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// 月报表
|
||||
if (_hasValidRecords)
|
||||
MonthlyReport(records: _records),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 检查是否有有效记录
|
||||
bool get _hasValidRecords => _records.isNotEmpty;
|
||||
}
|
||||
@ -91,32 +91,4 @@ class DatabaseService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 添加按日<EFBFBD><EFBFBD><EFBFBD>范围获取记录的方法
|
||||
Future<List<Record>> getRecordsByDateRange(DateTime startDate, DateTime endDate) async {
|
||||
final db = await database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'records',
|
||||
where: 'createTime BETWEEN ? AND ?',
|
||||
whereArgs: [
|
||||
startDate.toIso8601String(),
|
||||
endDate.toIso8601String(),
|
||||
],
|
||||
orderBy: 'createTime DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) {
|
||||
final map = maps[i];
|
||||
return Record(
|
||||
id: map['id'],
|
||||
type: RecordType.values[map['type']],
|
||||
categoryId: map['categoryId'],
|
||||
note: map['note'],
|
||||
amount: map['amount'],
|
||||
createTime: DateTime.parse(map['createTime']),
|
||||
accountId: map['accountId'],
|
||||
imageUrls: map['imageUrls']?.split(','),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
class DateFormatter {
|
||||
/// 格式化日期显示
|
||||
static 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;
|
||||
|
||||
// 格式化日期部分
|
||||
String dateStr;
|
||||
if (date.year == now.year) {
|
||||
// 当年日期显示 月-日
|
||||
dateStr = '${_padZero(date.month)}-${_padZero(date.day)}';
|
||||
} else {
|
||||
// 非当年日期显示 年-月-日
|
||||
dateStr = '${date.year}-${_padZero(date.month)}-${_padZero(date.day)}';
|
||||
}
|
||||
|
||||
// 获取提示文案
|
||||
String hint;
|
||||
switch (difference) {
|
||||
case 0:
|
||||
hint = '今天';
|
||||
break;
|
||||
case 1:
|
||||
hint = '昨天';
|
||||
break;
|
||||
case 2:
|
||||
hint = '前天';
|
||||
break;
|
||||
default:
|
||||
// 获取星期几
|
||||
hint = _getWeekDay(date);
|
||||
}
|
||||
|
||||
return '$dateStr $hint';
|
||||
}
|
||||
|
||||
/// 数字补零
|
||||
static String _padZero(int number) {
|
||||
return number.toString().padLeft(2, '0');
|
||||
}
|
||||
|
||||
/// 获取星期几
|
||||
static String _getWeekDay(DateTime date) {
|
||||
final weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
return weekDays[date.weekday % 7];
|
||||
}
|
||||
}
|
||||
@ -1,136 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/record.dart';
|
||||
|
||||
class MonthlySummaryCard extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
final bool isVisible;
|
||||
final VoidCallback onAddRecord;
|
||||
final Function(bool) onVisibilityChanged;
|
||||
|
||||
const MonthlySummaryCard({
|
||||
Key? key,
|
||||
required this.records,
|
||||
required this.isVisible,
|
||||
required this.onAddRecord,
|
||||
required this.onVisibilityChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
String _formatAmount(double amount, {bool showSign = false}) {
|
||||
if (!isVisible) return '¥****';
|
||||
String prefix = showSign && amount > 0 ? '+' : '';
|
||||
return '$prefix¥${amount.toStringAsFixed(2)}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final monthlyExpense = records
|
||||
.where((r) => r.type == RecordType.expense)
|
||||
.fold(0.0, (sum, r) => sum + r.amount);
|
||||
|
||||
final monthlyIncome = records
|
||||
.where((r) => r.type == RecordType.income)
|
||||
.fold(0.0, (sum, r) => sum + r.amount);
|
||||
|
||||
final monthlyBalance = monthlyIncome - monthlyExpense;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'月支出',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isVisible ? Icons.visibility : Icons.visibility_off,
|
||||
size: 20,
|
||||
color: Colors.black54,
|
||||
),
|
||||
onPressed: () => onVisibilityChanged(!isVisible),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_formatAmount(monthlyExpense),
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildSummaryItem('月收入', monthlyIncome),
|
||||
_buildSummaryItem('月结余', monthlyBalance, showSign: true),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
child: ElevatedButton(
|
||||
onPressed: onAddRecord,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF9FE2BF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'记一笔',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryItem(String label, double amount, {bool showSign = false}) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatAmount(amount, showSign: showSign),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -15,14 +15,14 @@ class RecordDetailDialog extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
/// 获取分类信息
|
||||
Category _getCategory(String categoryId, RecordType type) {
|
||||
return CategoryConfig.getCategoriesByType(type)
|
||||
Category _getCategory(String categoryId) {
|
||||
return [...expenseCategories, ...incomeCategories]
|
||||
.firstWhere((c) => c.id == categoryId);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final category = _getCategory(record.categoryId, record.type);
|
||||
final category = _getCategory(record.categoryId);
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
|
||||
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import '../models/record.dart';
|
||||
import '../data/categories.dart';
|
||||
import './record_detail_dialog.dart';
|
||||
import '../utils/date_formatter.dart';
|
||||
|
||||
class RecordList extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
@ -17,28 +16,29 @@ class RecordList extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
/// 获取分类信息
|
||||
Category _getCategory(String categoryId, RecordType type) {
|
||||
return CategoryConfig.getCategoriesByType(type)
|
||||
Category _getCategory(String categoryId) {
|
||||
return [...expenseCategories, ...incomeCategories]
|
||||
.firstWhere((c) => c.id == categoryId);
|
||||
}
|
||||
|
||||
/// 按日期分组记录
|
||||
Map<DateTime, List<Record>> _groupByDate() {
|
||||
final groups = <DateTime, List<Record>>{};
|
||||
Map<String, List<Record>> _groupByDate() {
|
||||
final groups = <String, List<Record>>{};
|
||||
for (final record in records) {
|
||||
final dateKey = DateTime(
|
||||
record.createTime.year,
|
||||
record.createTime.month,
|
||||
record.createTime.day,
|
||||
);
|
||||
if (!groups.containsKey(dateKey)) {
|
||||
groups[dateKey] = [];
|
||||
final date = _formatDate(record.createTime);
|
||||
if (!groups.containsKey(date)) {
|
||||
groups[date] = [];
|
||||
}
|
||||
groups[dateKey]!.add(record);
|
||||
groups[date]!.add(record);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 计算日汇总
|
||||
String _getDailySummary(List<Record> dayRecords) {
|
||||
double expense = 0;
|
||||
@ -60,15 +60,13 @@ class RecordList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final groups = _groupByDate();
|
||||
final dates = groups.keys.toList()
|
||||
..sort((a, b) => b.compareTo(a));
|
||||
final dates = groups.keys.toList()..sort((a, b) => b.compareTo(a));
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: dates.length,
|
||||
itemBuilder: (context, index) {
|
||||
final dateKey = dates[index];
|
||||
final dayRecords = groups[dateKey]!;
|
||||
final formattedDate = DateFormatter.formatDate(dateKey);
|
||||
final date = dates[index];
|
||||
final dayRecords = groups[date]!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -80,23 +78,19 @@ class RecordList extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
formattedDate,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_getDailySummary(dayRecords),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
date,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(_getDailySummary(dayRecords)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 记录列表
|
||||
...dayRecords.map((record) {
|
||||
final category = _getCategory(record.categoryId, record.type);
|
||||
final category = _getCategory(record.categoryId);
|
||||
return ListTile(
|
||||
leading: Icon(category.icon),
|
||||
title: Text(category.name),
|
||||
|
||||
@ -1,225 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/record.dart';
|
||||
import '../../data/categories.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class CategoryChart extends StatefulWidget {
|
||||
final List<Record> records;
|
||||
|
||||
const CategoryChart({
|
||||
Key? key,
|
||||
required this.records,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CategoryChart> createState() => _CategoryChartState();
|
||||
}
|
||||
|
||||
class _CategoryChartState extends State<CategoryChart> {
|
||||
bool _showExpense = true; // true显示支出,false显示收入
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoryData = _getCategoryData();
|
||||
if (categoryData.isEmpty) return const SizedBox();
|
||||
|
||||
final isYearView = widget.records.any((r) =>
|
||||
r.createTime.month != widget.records.first.createTime.month);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${isYearView ? "年度" : ""}分类统计',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ToggleButtons(
|
||||
isSelected: [_showExpense, !_showExpense],
|
||||
onPressed: (index) {
|
||||
setState(() {
|
||||
_showExpense = index == 0;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
selectedColor: Theme.of(context).primaryColor,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 60,
|
||||
minHeight: 32,
|
||||
),
|
||||
children: const [
|
||||
Text('支出'),
|
||||
Text('收入'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: CustomPaint(
|
||||
size: const Size(200, 200),
|
||||
painter: PieChartPainter(
|
||||
categories: categoryData,
|
||||
total: categoryData.fold(
|
||||
0.0,
|
||||
(sum, item) => sum + item.amount,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildLegend(categoryData),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<CategoryData> _getCategoryData() {
|
||||
final Map<String, double> categoryAmounts = {};
|
||||
final targetType = _showExpense ? RecordType.expense : RecordType.income;
|
||||
|
||||
// 统计各分类金额
|
||||
for (final record in widget.records) {
|
||||
if (record.type == targetType) {
|
||||
categoryAmounts[record.categoryId] =
|
||||
(categoryAmounts[record.categoryId] ?? 0) + record.amount;
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为列表并排序
|
||||
final categories = CategoryConfig.getCategoriesByType(targetType);
|
||||
return categoryAmounts.entries.map((entry) {
|
||||
final category = categories.firstWhere(
|
||||
(c) => c.id == entry.key,
|
||||
orElse: () => Category(
|
||||
id: 'unknown',
|
||||
name: '未知',
|
||||
iconKey: 'other',
|
||||
type: targetType,
|
||||
sortOrder: 999,
|
||||
),
|
||||
);
|
||||
return CategoryData(
|
||||
category: category,
|
||||
amount: entry.value,
|
||||
);
|
||||
}).toList()
|
||||
..sort((a, b) => b.amount.compareTo(a.amount));
|
||||
}
|
||||
|
||||
Widget _buildLegend(List<CategoryData> data) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = data[index];
|
||||
final total = data.fold(0.0, (sum, item) => sum + item.amount);
|
||||
final percentage = (item.amount / total * 100).toStringAsFixed(1);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.primaries[index % Colors.primaries.length],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.category.name,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$percentage%',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryData {
|
||||
final Category category;
|
||||
final double amount;
|
||||
|
||||
CategoryData({
|
||||
required this.category,
|
||||
required this.amount,
|
||||
});
|
||||
}
|
||||
|
||||
class PieChartPainter extends CustomPainter {
|
||||
final List<CategoryData> categories;
|
||||
final double total;
|
||||
|
||||
PieChartPainter({
|
||||
required this.categories,
|
||||
required this.total,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = math.min(size.width, size.height) / 2;
|
||||
|
||||
var startAngle = -math.pi / 2;
|
||||
|
||||
for (var i = 0; i < categories.length; i++) {
|
||||
final sweepAngle = 2 * math.pi * categories[i].amount / total;
|
||||
|
||||
final paint = Paint()
|
||||
..color = Colors.primaries[i % Colors.primaries.length]
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
startAngle,
|
||||
sweepAngle,
|
||||
true,
|
||||
paint,
|
||||
);
|
||||
|
||||
startAngle += sweepAngle;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@ -1,227 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/record.dart';
|
||||
|
||||
class DailyChart extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
|
||||
const DailyChart({
|
||||
Key? key,
|
||||
required this.records,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 按日期分组数据
|
||||
final dailyData = _getDailyData();
|
||||
if (dailyData.isEmpty) return const SizedBox();
|
||||
|
||||
// 计算最大值用于缩放
|
||||
final maxAmount = dailyData.fold(0.0, (max, data) {
|
||||
final dayMax = data.income > data.expense ? data.income : data.expense;
|
||||
return dayMax > max ? dayMax : max;
|
||||
});
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'每日收支',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CustomPaint(
|
||||
size: const Size(double.infinity, 200),
|
||||
painter: ChartPainter(
|
||||
dailyData: dailyData,
|
||||
maxAmount: maxAmount,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegend('支出', Colors.red),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegend('收入', Colors.green),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DailyData> _getDailyData() {
|
||||
final Map<DateTime, DailyData> dailyMap = {};
|
||||
|
||||
// 获取月份的第一天和最后一天
|
||||
final firstRecord = records.first;
|
||||
final firstDay = DateTime(firstRecord.createTime.year, firstRecord.createTime.month, 1);
|
||||
final lastDay = DateTime(firstRecord.createTime.year, firstRecord.createTime.month + 1, 0);
|
||||
|
||||
// 初始化每一天的数据
|
||||
for (var day = firstDay; day.isBefore(lastDay.add(const Duration(days: 1))); day = day.add(const Duration(days: 1))) {
|
||||
dailyMap[DateTime(day.year, day.month, day.day)] = DailyData(
|
||||
date: day,
|
||||
expense: 0,
|
||||
income: 0,
|
||||
);
|
||||
}
|
||||
|
||||
// 填充实际数据
|
||||
for (final record in records) {
|
||||
final date = DateTime(record.createTime.year, record.createTime.month, record.createTime.day);
|
||||
final data = dailyMap[date]!;
|
||||
if (record.type == RecordType.expense) {
|
||||
data.expense += record.amount;
|
||||
} else {
|
||||
data.income += record.amount;
|
||||
}
|
||||
}
|
||||
|
||||
return dailyMap.values.toList();
|
||||
}
|
||||
|
||||
Widget _buildLegend(String label, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(label),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DailyData {
|
||||
final DateTime date;
|
||||
double expense;
|
||||
double income;
|
||||
|
||||
DailyData({
|
||||
required this.date,
|
||||
this.expense = 0,
|
||||
this.income = 0,
|
||||
});
|
||||
}
|
||||
|
||||
class ChartPainter extends CustomPainter {
|
||||
final List<DailyData> dailyData;
|
||||
final double maxAmount;
|
||||
|
||||
ChartPainter({
|
||||
required this.dailyData,
|
||||
required this.maxAmount,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final expensePath = Path();
|
||||
final incomePath = Path();
|
||||
final expensePaint = Paint()
|
||||
..color = Colors.red.withOpacity(0.8)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
final incomePaint = Paint()
|
||||
..color = Colors.green.withOpacity(0.8)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
final width = size.width;
|
||||
final height = size.height - 20; // 留出底部空间显示日期
|
||||
final dayWidth = width / (dailyData.length - 1);
|
||||
|
||||
// 绘制网格线
|
||||
final gridPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.2)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1;
|
||||
|
||||
// 横向网格线
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
final y = height * i / 4;
|
||||
canvas.drawLine(
|
||||
Offset(0, y),
|
||||
Offset(width, y),
|
||||
gridPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// 绘制折线
|
||||
for (var i = 0; i < dailyData.length; i++) {
|
||||
final x = i * dayWidth;
|
||||
final expenseY = height - (height * dailyData[i].expense / maxAmount);
|
||||
final incomeY = height - (height * dailyData[i].income / maxAmount);
|
||||
|
||||
if (i == 0) {
|
||||
expensePath.moveTo(x, expenseY);
|
||||
incomePath.moveTo(x, incomeY);
|
||||
} else {
|
||||
expensePath.lineTo(x, expenseY);
|
||||
incomePath.lineTo(x, incomeY);
|
||||
}
|
||||
|
||||
// 绘制数据点
|
||||
canvas.drawCircle(
|
||||
Offset(x, expenseY),
|
||||
3,
|
||||
Paint()..color = Colors.red,
|
||||
);
|
||||
canvas.drawCircle(
|
||||
Offset(x, incomeY),
|
||||
3,
|
||||
Paint()..color = Colors.green,
|
||||
);
|
||||
|
||||
// 绘制日期
|
||||
if (i % 5 == 0 || i == dailyData.length - 1) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '${dailyData[i].date.day}日',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(x - textPainter.width / 2, height + 5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawPath(expensePath, expensePaint);
|
||||
canvas.drawPath(incomePath, incomePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/record.dart';
|
||||
import '../../utils/date_formatter.dart';
|
||||
|
||||
class DailyReport extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
|
||||
const DailyReport({
|
||||
Key? key,
|
||||
required this.records,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dailyData = _getDailyData();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'日报表',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FlexColumnWidth(2),
|
||||
1: FlexColumnWidth(2),
|
||||
2: FlexColumnWidth(2),
|
||||
3: FlexColumnWidth(2),
|
||||
},
|
||||
children: [
|
||||
const TableRow(
|
||||
children: [
|
||||
Text('日期', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('收入', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('支出', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('结余', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
...dailyData.map((data) => TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(DateFormatter.formatDate(data.date)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'¥${data.income.toStringAsFixed(2)}',
|
||||
style: const TextStyle(color: Colors.green),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'¥${data.expense.toStringAsFixed(2)}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'¥${(data.income - data.expense).toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: data.income >= data.expense
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)).toList(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DailyData> _getDailyData() {
|
||||
final Map<DateTime, DailyData> dailyMap = {};
|
||||
|
||||
for (final record in records) {
|
||||
final date = DateTime(
|
||||
record.createTime.year,
|
||||
record.createTime.month,
|
||||
record.createTime.day,
|
||||
);
|
||||
|
||||
dailyMap.putIfAbsent(
|
||||
date,
|
||||
() => DailyData(date: date),
|
||||
);
|
||||
|
||||
if (record.type == RecordType.expense) {
|
||||
dailyMap[date]!.expense += record.amount;
|
||||
} else {
|
||||
dailyMap[date]!.income += record.amount;
|
||||
}
|
||||
}
|
||||
|
||||
return dailyMap.values.toList()
|
||||
..sort((a, b) => b.date.compareTo(a.date));
|
||||
}
|
||||
}
|
||||
|
||||
class DailyData {
|
||||
final DateTime date;
|
||||
double expense;
|
||||
double income;
|
||||
|
||||
DailyData({
|
||||
required this.date,
|
||||
this.expense = 0,
|
||||
this.income = 0,
|
||||
});
|
||||
}
|
||||
@ -1,244 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/record.dart';
|
||||
|
||||
class MonthlyChart extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
|
||||
const MonthlyChart({
|
||||
Key? key,
|
||||
required this.records,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 按月份分组数据
|
||||
final monthlyData = _getMonthlyData();
|
||||
if (monthlyData.isEmpty) return const SizedBox();
|
||||
|
||||
// 计算最大值用于缩放
|
||||
final maxAmount = monthlyData.fold(0.0, (max, data) {
|
||||
final monthMax = data.income > data.expense ? data.income : data.expense;
|
||||
return monthMax > max ? monthMax : max;
|
||||
});
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'每月收支',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CustomPaint(
|
||||
size: const Size(double.infinity, 200),
|
||||
painter: ChartPainter(
|
||||
monthlyData: monthlyData,
|
||||
maxAmount: maxAmount,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegend('支出', Colors.red),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegend('收入', Colors.green),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<MonthData> _getMonthlyData() {
|
||||
final Map<int, MonthData> monthlyMap = {};
|
||||
|
||||
// 初始化12个月的数据
|
||||
for (int month = 1; month <= 12; month++) {
|
||||
monthlyMap[month] = MonthData(month: month);
|
||||
}
|
||||
|
||||
// 填充实际数据
|
||||
for (final record in records) {
|
||||
final month = record.createTime.month;
|
||||
if (record.type == RecordType.expense) {
|
||||
monthlyMap[month]!.expense += record.amount;
|
||||
} else {
|
||||
monthlyMap[month]!.income += record.amount;
|
||||
}
|
||||
}
|
||||
|
||||
return monthlyMap.values.toList();
|
||||
}
|
||||
|
||||
Widget _buildLegend(String label, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(label),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MonthData {
|
||||
final int month;
|
||||
double expense;
|
||||
double income;
|
||||
|
||||
MonthData({
|
||||
required this.month,
|
||||
this.expense = 0,
|
||||
this.income = 0,
|
||||
});
|
||||
}
|
||||
|
||||
class ChartPainter extends CustomPainter {
|
||||
final List<MonthData> monthlyData;
|
||||
final double maxAmount;
|
||||
|
||||
ChartPainter({
|
||||
required this.monthlyData,
|
||||
required this.maxAmount,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final expensePath = Path();
|
||||
final incomePath = Path();
|
||||
final expensePaint = Paint()
|
||||
..color = Colors.red.withOpacity(0.8)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
final incomePaint = Paint()
|
||||
..color = Colors.green.withOpacity(0.8)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
final width = size.width;
|
||||
final height = size.height - 20; // 留出底部空间显示月份
|
||||
final monthWidth = width / 11; // 12个月,11个间隔
|
||||
|
||||
// 绘制网格线
|
||||
final gridPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.2)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1;
|
||||
|
||||
// 横向网格线
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
final y = height * i / 4;
|
||||
canvas.drawLine(
|
||||
Offset(0, y),
|
||||
Offset(width, y),
|
||||
gridPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// 添加金额标签
|
||||
void drawAmountLabel(double x, double y, double amount) {
|
||||
if (amount == 0) return;
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: amount >= 1000
|
||||
? '${(amount / 1000).toStringAsFixed(1)}k'
|
||||
: amount.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(x - textPainter.width / 2, y - 15),
|
||||
);
|
||||
}
|
||||
|
||||
// 绘制折线和数据点
|
||||
for (var i = 0; i < monthlyData.length; i++) {
|
||||
final x = i * monthWidth;
|
||||
final expenseY = height - (height * monthlyData[i].expense / maxAmount);
|
||||
final incomeY = height - (height * monthlyData[i].income / maxAmount);
|
||||
|
||||
if (i == 0) {
|
||||
expensePath.moveTo(x, expenseY);
|
||||
incomePath.moveTo(x, incomeY);
|
||||
} else {
|
||||
expensePath.lineTo(x, expenseY);
|
||||
incomePath.lineTo(x, incomeY);
|
||||
}
|
||||
|
||||
// 绘制数据点和金额标签
|
||||
if (monthlyData[i].expense > 0) {
|
||||
canvas.drawCircle(
|
||||
Offset(x, expenseY),
|
||||
3,
|
||||
Paint()..color = Colors.red,
|
||||
);
|
||||
drawAmountLabel(x, expenseY, monthlyData[i].expense);
|
||||
}
|
||||
|
||||
if (monthlyData[i].income > 0) {
|
||||
canvas.drawCircle(
|
||||
Offset(x, incomeY),
|
||||
3,
|
||||
Paint()..color = Colors.green,
|
||||
);
|
||||
drawAmountLabel(x, incomeY, monthlyData[i].income);
|
||||
}
|
||||
|
||||
// 绘制月份
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '${monthlyData[i].month}月',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(x - textPainter.width / 2, height + 5),
|
||||
);
|
||||
}
|
||||
|
||||
canvas.drawPath(expensePath, expensePaint);
|
||||
canvas.drawPath(incomePath, incomePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/record.dart';
|
||||
|
||||
class MonthlyOverview extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
final bool isYearView;
|
||||
|
||||
const MonthlyOverview({
|
||||
Key? key,
|
||||
required this.records,
|
||||
this.isYearView = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalExpense = records
|
||||
.where((r) => r.type == RecordType.expense)
|
||||
.fold(0.0, (sum, r) => sum + r.amount);
|
||||
|
||||
final totalIncome = records
|
||||
.where((r) => r.type == RecordType.income)
|
||||
.fold(0.0, (sum, r) => sum + r.amount);
|
||||
|
||||
final balance = totalIncome - totalExpense;
|
||||
|
||||
// 计算日均支出
|
||||
final daysInPeriod = isYearView
|
||||
? (records.isNotEmpty ? _getDaysInYear(records.first.createTime.year) : 365)
|
||||
: (records.isNotEmpty ? _getDaysInMonth(records.first.createTime) : 30);
|
||||
final dailyAverage = totalExpense / daysInPeriod;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildOverviewItem(
|
||||
label: '${isYearView ? "年" : "月"}支出',
|
||||
amount: totalExpense,
|
||||
textColor: Colors.red,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildOverviewItem(
|
||||
label: '${isYearView ? "年" : "月"}收入',
|
||||
amount: totalIncome,
|
||||
textColor: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildOverviewItem(
|
||||
label: '结余',
|
||||
amount: balance,
|
||||
textColor: balance >= 0 ? Colors.green : Colors.red,
|
||||
showSign: true,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildOverviewItem(
|
||||
label: '日均支出',
|
||||
amount: dailyAverage,
|
||||
textColor: Colors.grey[800]!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _getDaysInMonth(DateTime date) {
|
||||
return DateTime(date.year, date.month + 1, 0).day;
|
||||
}
|
||||
|
||||
int _getDaysInYear(int year) {
|
||||
return DateTime(year).isLeapYear ? 366 : 365;
|
||||
}
|
||||
|
||||
Widget _buildOverviewItem({
|
||||
required String label,
|
||||
required double amount,
|
||||
required Color textColor,
|
||||
bool showSign = false,
|
||||
}) {
|
||||
String amountText = '¥${amount.toStringAsFixed(2)}';
|
||||
if (showSign && amount > 0) {
|
||||
amountText = '+$amountText';
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
amountText,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension DateTimeExtension on DateTime {
|
||||
bool get isLeapYear {
|
||||
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
|
||||
}
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/record.dart';
|
||||
|
||||
class MonthlyReport extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
|
||||
const MonthlyReport({
|
||||
Key? key,
|
||||
required this.records,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final monthlyData = _getMonthlyData();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'月报表',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FlexColumnWidth(1.5),
|
||||
1: FlexColumnWidth(2),
|
||||
2: FlexColumnWidth(2),
|
||||
3: FlexColumnWidth(2),
|
||||
},
|
||||
children: [
|
||||
const TableRow(
|
||||
children: [
|
||||
Text('月份', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('收入', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('支出', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('结余', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
...monthlyData.map((data) => TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('${data.month}月'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'¥${data.income.toStringAsFixed(2)}',
|
||||
style: const TextStyle(color: Colors.green),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'¥${data.expense.toStringAsFixed(2)}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'¥${(data.income - data.expense).toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: data.income >= data.expense
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)).toList(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<MonthData> _getMonthlyData() {
|
||||
final Map<int, MonthData> monthlyMap = {};
|
||||
|
||||
// 初始化12个月的数据
|
||||
for (int month = 1; month <= 12; month++) {
|
||||
monthlyMap[month] = MonthData(month: month);
|
||||
}
|
||||
|
||||
// 填充实际数据
|
||||
for (final record in records) {
|
||||
final month = record.createTime.month;
|
||||
if (record.type == RecordType.expense) {
|
||||
monthlyMap[month]!.expense += record.amount;
|
||||
} else {
|
||||
monthlyMap[month]!.income += record.amount;
|
||||
}
|
||||
}
|
||||
|
||||
return monthlyMap.values.toList()
|
||||
..sort((a, b) => a.month.compareTo(b.month));
|
||||
}
|
||||
}
|
||||
|
||||
class MonthData {
|
||||
final int month;
|
||||
double expense;
|
||||
double income;
|
||||
|
||||
MonthData({
|
||||
required this.month,
|
||||
this.expense = 0,
|
||||
this.income = 0,
|
||||
});
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ViewSelector extends StatelessWidget {
|
||||
final bool isMonthView;
|
||||
final DateTime selectedDate;
|
||||
final Function(bool) onViewChanged;
|
||||
final Function(DateTime) onDateChanged;
|
||||
|
||||
const ViewSelector({
|
||||
Key? key,
|
||||
required this.isMonthView,
|
||||
required this.selectedDate,
|
||||
required this.onViewChanged,
|
||||
required this.onDateChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 视图切换
|
||||
ToggleButtons(
|
||||
isSelected: [isMonthView, !isMonthView],
|
||||
onPressed: (index) => onViewChanged(index == 0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
selectedColor: Theme.of(context).primaryColor,
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text('月'),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text('年'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 日期选择器
|
||||
TextButton(
|
||||
onPressed: () => _showDatePicker(context),
|
||||
child: Text(
|
||||
isMonthView
|
||||
? '${selectedDate.year}年${selectedDate.month}月'
|
||||
: '${selectedDate.year}年',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showDatePicker(BuildContext context) async {
|
||||
if (isMonthView) {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
initialDatePickerMode: DatePickerMode.year,
|
||||
);
|
||||
if (picked != null) {
|
||||
onDateChanged(DateTime(picked.year, picked.month));
|
||||
}
|
||||
} else {
|
||||
final DateTime? picked = await showDialog<DateTime>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('选择年份'),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
height: 300,
|
||||
child: YearPicker(
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
selectedDate: selectedDate,
|
||||
onChanged: (DateTime value) {
|
||||
Navigator.pop(context, value);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (picked != null) {
|
||||
onDateChanged(DateTime(picked.year));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/record.dart';
|
||||
import '../services/database_service.dart';
|
||||
|
||||
class WeeklySpendingChart extends StatelessWidget {
|
||||
final List<Record> records;
|
||||
late final DatabaseService _dbService;
|
||||
|
||||
WeeklySpendingChart({
|
||||
Key? key,
|
||||
required this.records,
|
||||
}) : super(key: key) {
|
||||
_dbService = DatabaseService();
|
||||
}
|
||||
|
||||
/// 获取最近7天的数据
|
||||
Future<List<DailySpending>> _getWeeklyData() async {
|
||||
final now = DateTime.now();
|
||||
final startDate = DateTime(now.year, now.month, now.day - 6);
|
||||
final endDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
|
||||
// 直接从数据库获取最近7天的数据
|
||||
final weekRecords = await _dbService.getRecordsByDateRange(startDate, endDate);
|
||||
|
||||
final List<DailySpending> weeklyData = [];
|
||||
for (int i = 6; i >= 0; i--) {
|
||||
final date = DateTime(now.year, now.month, now.day - i);
|
||||
final dayRecords = weekRecords.where((record) =>
|
||||
record.createTime.year == date.year &&
|
||||
record.createTime.month == date.month &&
|
||||
record.createTime.day == date.day &&
|
||||
record.type == RecordType.expense
|
||||
);
|
||||
|
||||
final dailyTotal = dayRecords.fold(0.0, (sum, record) => sum + record.amount);
|
||||
weeklyData.add(DailySpending(
|
||||
date: date,
|
||||
amount: dailyTotal,
|
||||
));
|
||||
}
|
||||
|
||||
return weeklyData;
|
||||
}
|
||||
|
||||
String _formatAmount(double amount) {
|
||||
if (amount >= 10000) {
|
||||
return '${(amount / 10000).toStringAsFixed(2)}w';
|
||||
} else if (amount >= 1000) {
|
||||
return '${(amount / 1000).toStringAsFixed(2)}k';
|
||||
}
|
||||
return amount.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<DailySpending>>(
|
||||
future: _getWeeklyData(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final weeklyData = snapshot.data!;
|
||||
final totalSpending = weeklyData.fold(0.0, (sum, day) => sum + day.amount);
|
||||
final maxDailySpending = weeklyData.fold(0.0, (max, day) => day.amount > max ? day.amount : max);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'最近7日支出',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'总计: ${totalSpending.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 140,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: weeklyData.map((day) {
|
||||
final barHeight = maxDailySpending > 0
|
||||
? (day.amount / maxDailySpending) * 80
|
||||
: 0.0;
|
||||
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatAmount(day.amount),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
height: 1.2,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: 24,
|
||||
height: barHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.8),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getWeekdayText(day.date.weekday),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getWeekdayText(int weekday) {
|
||||
const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
return weekdays[weekday - 1];
|
||||
}
|
||||
}
|
||||
|
||||
class DailySpending {
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
|
||||
DailySpending({
|
||||
required this.date,
|
||||
required this.amount,
|
||||
});
|
||||
}
|
||||
114
pubspec.lock
114
pubspec.lock
@ -6,7 +6,7 @@ packages:
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
boolean_selector:
|
||||
@ -14,7 +14,7 @@ packages:
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
characters:
|
||||
@ -22,7 +22,7 @@ packages:
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
clock:
|
||||
@ -30,7 +30,7 @@ packages:
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
@ -38,7 +38,7 @@ packages:
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
crypto:
|
||||
@ -46,7 +46,7 @@ packages:
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
cupertino_icons:
|
||||
@ -54,39 +54,31 @@ packages:
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
decimal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: decimal
|
||||
sha256: "4140a688f9e443e2f4de3a1162387bf25e1ac6d51e24c9da263f245210f41440"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "093b02a284b4969bb641a6236bbb8e626e4035c6ec9e30c20b65d505c24b3080"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
|
||||
url: "https://pub.dev"
|
||||
sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
version: "7.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
@ -94,7 +86,7 @@ packages:
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
file:
|
||||
@ -102,7 +94,7 @@ packages:
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
@ -110,7 +102,7 @@ packages:
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
@ -123,7 +115,7 @@ packages:
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_test:
|
||||
@ -136,20 +128,12 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
@ -157,7 +141,7 @@ packages:
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
@ -165,7 +149,7 @@ packages:
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
material_color_utilities:
|
||||
@ -173,7 +157,7 @@ packages:
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
meta:
|
||||
@ -181,7 +165,7 @@ packages:
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
path:
|
||||
@ -189,7 +173,7 @@ packages:
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
plugin_platform_interface:
|
||||
@ -197,23 +181,15 @@ packages:
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
rational:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rational
|
||||
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
sensors_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sensors_plus
|
||||
sha256: "362c8f4f001838b90dd5206b898bbad941bc0142479eab9a3415f0f79e622908"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sensors_plus_platform_interface:
|
||||
@ -221,7 +197,7 @@ packages:
|
||||
description:
|
||||
name: sensors_plus_platform_interface
|
||||
sha256: bc472d6cfd622acb4f020e726433ee31788b038056691ba433fec80e448a094f
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
sensors_plus_web:
|
||||
@ -229,7 +205,7 @@ packages:
|
||||
description:
|
||||
name: sensors_plus_web
|
||||
sha256: fca8d7d9ab6233b2a059952666415508e252420be1ef54f092d07884da53ec5e
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
sky_engine:
|
||||
@ -242,7 +218,7 @@ packages:
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
sprintf:
|
||||
@ -250,7 +226,7 @@ packages:
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
@ -258,7 +234,7 @@ packages:
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
sqflite_common:
|
||||
@ -266,7 +242,7 @@ packages:
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
stack_trace:
|
||||
@ -274,7 +250,7 @@ packages:
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
@ -282,7 +258,7 @@ packages:
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
string_scanner:
|
||||
@ -290,7 +266,7 @@ packages:
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
synchronized:
|
||||
@ -298,7 +274,7 @@ packages:
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
term_glyph:
|
||||
@ -306,7 +282,7 @@ packages:
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
@ -314,7 +290,7 @@ packages:
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
typed_data:
|
||||
@ -322,7 +298,7 @@ packages:
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
uuid:
|
||||
@ -330,7 +306,7 @@ packages:
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_math:
|
||||
@ -338,7 +314,7 @@ packages:
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vibration:
|
||||
@ -346,7 +322,7 @@ packages:
|
||||
description:
|
||||
name: vibration
|
||||
sha256: "06588a845a4ebc73ab7ff7da555c2b3dbcd9676164b5856a38bf0b2287f1045d"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
vibration_platform_interface:
|
||||
@ -354,7 +330,7 @@ packages:
|
||||
description:
|
||||
name: vibration_platform_interface
|
||||
sha256: f66b39aab2447038978c16f3d6f77228e49ef5717556e3da02313e044e4a7600
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "0.0.2"
|
||||
web:
|
||||
@ -362,7 +338,7 @@ packages:
|
||||
description:
|
||||
name: web
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
win32:
|
||||
@ -370,7 +346,7 @@ packages:
|
||||
description:
|
||||
name: win32
|
||||
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
win32_registry:
|
||||
@ -378,9 +354,9 @@ packages:
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
sdks:
|
||||
dart: ">=3.2.5 <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
||||
@ -35,7 +35,6 @@ dependencies:
|
||||
uuid: ^4.3.3 # 添加uuid包用于生成唯一ID
|
||||
sqflite: ^2.3.2 # 添加 SQLite 支持
|
||||
path: ^1.8.3 # 用于处理文件路径
|
||||
decimal: ^3.0.0 # 处理计算精度
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user