新增近7日支出 和月度收支

This commit is contained in:
ddshi 2024-12-25 14:06:42 +08:00
parent edef638500
commit 2a2857132b
5 changed files with 401 additions and 30 deletions

View File

@ -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
## 项目结构

View File

@ -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),
),
);
}
@ -269,4 +267,4 @@ class _HomePageState extends State<HomePage> {
_accelerometerSubscription?.cancel();
super.dispose();
}
}
}

View 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,
),
),
],
);
}
}

View 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,
});
}

View File

@ -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: