新增近7日支出 和月度收支
This commit is contained in:
parent
edef638500
commit
2a2857132b
58
README.md
58
README.md
@ -1,16 +1,54 @@
|
||||
# swing_account
|
||||
# Swing Account (摇摇记账)
|
||||
|
||||
A new Flutter project.
|
||||
一个简单易用的记账应用,支持摇一摇快速记账。
|
||||
|
||||
## Getting Started
|
||||
## 功能特点
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
### 记账功能
|
||||
- 支持收入/支出记录
|
||||
- 摇一摇快速记账
|
||||
- 记录分类管理
|
||||
- 备注和图片附件
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
### 数据统计
|
||||
- 月度收支统计
|
||||
* 月支出总额
|
||||
* 月收入总额
|
||||
* 月结余计算
|
||||
* 金额显示/隐藏切换
|
||||
- 最近7日支出统计
|
||||
* 柱状图显示
|
||||
* 自动计算比例
|
||||
* 支持金额缩写(k/w)
|
||||
- 按日期分组的记录列表
|
||||
* 日期分组显示
|
||||
* 每日收支统计
|
||||
* 支持编辑和删除
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
### 界面交互
|
||||
- 月份筛选
|
||||
- 记录详情查看
|
||||
- 支持记录编辑/删除
|
||||
- 响应式布局设计
|
||||
|
||||
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.
|
||||
## 技术特点
|
||||
|
||||
### 核心功能
|
||||
- 使用 SQLite 本地数据存储
|
||||
- 传感器集成实现摇一摇
|
||||
- 支持图片附件存储
|
||||
- 精确的数值计算
|
||||
|
||||
### 依赖包
|
||||
- sensors_plus: 传感器支持
|
||||
- vibration: 震动反馈
|
||||
- sqflite: 数据库
|
||||
- decimal: 精确计算
|
||||
- uuid: 唯一标识
|
||||
- path: 文件路径处理
|
||||
|
||||
## 开发环境
|
||||
- Flutter SDK: >=3.2.5 <4.0.0
|
||||
- Dart SDK: >=3.2.5 <4.0.0
|
||||
|
||||
## 项目结构
|
||||
|
||||
@ -7,6 +7,8 @@ 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);
|
||||
@ -23,10 +25,11 @@ class _HomePageState extends State<HomePage> {
|
||||
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 double _directionThreshold = 2.0; // 方向改变的最小加速
|
||||
static const int _sampleSize = 3; // 采样数量
|
||||
static const Duration _shakeWindow = Duration(milliseconds: 800);
|
||||
static const Duration _cooldown = Duration(milliseconds: 50);
|
||||
@ -61,7 +64,7 @@ class _HomePageState extends State<HomePage> {
|
||||
_loadRecords();
|
||||
}
|
||||
|
||||
/// 重置摇<EFBFBD><EFBFBD>检测状态
|
||||
/// 重置摇晃检测状态
|
||||
void _resetShakeDetection() {
|
||||
_lastShakeTime = null;
|
||||
_recentXValues.clear();
|
||||
@ -234,20 +237,19 @@ class _HomePageState extends State<HomePage> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildMonthSelector(),
|
||||
Text(
|
||||
'总支出: ¥${records.fold(0.0, (sum, record) => sum + record.amount).toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: _buildMonthSelector(),
|
||||
),
|
||||
MonthlySummaryCard(
|
||||
records: records,
|
||||
isVisible: _isAmountVisible,
|
||||
onAddRecord: () => _navigateToRecordPage(),
|
||||
onVisibilityChanged: (value) {
|
||||
setState(() {
|
||||
_isAmountVisible = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
WeeklySpendingChart(records: records),
|
||||
Expanded(
|
||||
child: RecordList(
|
||||
records: records,
|
||||
@ -257,10 +259,6 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _navigateToRecordPage(),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
136
lib/widgets/monthly_summary_card.dart
Normal file
136
lib/widgets/monthly_summary_card.dart
Normal file
@ -0,0 +1,136 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
175
lib/widgets/weekly_spending_chart.dart
Normal file
175
lib/widgets/weekly_spending_chart.dart
Normal file
@ -0,0 +1,175 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
24
pubspec.lock
24
pubspec.lock
@ -57,6 +57,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
@ -128,6 +136,14 @@ 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:
|
||||
@ -184,6 +200,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user