first commit

This commit is contained in:
2026-09-21 17:37:53 +08:00
commit 67d4dd5b7c
141 changed files with 8558 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
import 'json_util.dart';
/// 统计响应(对应后端 StatisticsResponse)
///
/// 口径说明:后端的 totalIncome / totalExpense / categoryStatistics 是按
/// **分类的 type** 判定收支的,而 /bills 列表是按 **账单的 type** 判定。
/// 两者可能不一致——前端不做交叉校验,汇总/饼图一律以此接口为准。
class StatisticsResponse {
final double totalIncome;
final double totalExpense;
final double balance;
final List<CategoryStat> categoryStatistics;
final List<DailyStat> dailyStatistics;
const StatisticsResponse({
required this.totalIncome,
required this.totalExpense,
required this.balance,
required this.categoryStatistics,
required this.dailyStatistics,
});
bool get isEmpty => categoryStatistics.isEmpty && totalIncome == 0 && totalExpense == 0;
factory StatisticsResponse.fromJson(Map<String, dynamic> json) {
final income = asDouble(json['totalIncome']);
final expense = asDouble(json['totalExpense']);
final balanceRaw = json['balance'];
return StatisticsResponse(
totalIncome: income,
totalExpense: expense,
// 后端没给 balance 时本地兜底计算
balance: balanceRaw == null ? income - expense : asDouble(balanceRaw),
categoryStatistics:
asMapList(json['categoryStatistics']).map(CategoryStat.fromJson).toList(),
dailyStatistics:
asMapList(json['dailyStatistics']).map(DailyStat.fromJson).toList(),
);
}
static const empty = StatisticsResponse(
totalIncome: 0,
totalExpense: 0,
balance: 0,
categoryStatistics: [],
dailyStatistics: [],
);
}
/// 分类维度的统计项
class CategoryStat {
final int categoryId;
final String categoryName;
final String categoryIcon;
final double amount;
final int type; // 1=支出 2=收入
const CategoryStat({
required this.categoryId,
required this.categoryName,
required this.categoryIcon,
required this.amount,
required this.type,
});
bool get isExpense => type != 2;
factory CategoryStat.fromJson(Map<String, dynamic> json) => CategoryStat(
categoryId: asInt(json['categoryId']),
categoryName: asNonEmpty(json['categoryName'], '未分类'),
categoryIcon: asNonEmpty(json['categoryIcon'], '📦'),
amount: asDouble(json['amount']),
type: asInt(json['type'], fallback: 1) == 2 ? 2 : 1,
);
}
/// 按日趋势项(本期统计页不画折线图,保留以便后续扩展)
class DailyStat {
final String date; // yyyy-MM-dd
final double income;
final double expense;
const DailyStat({required this.date, required this.income, required this.expense});
factory DailyStat.fromJson(Map<String, dynamic> json) => DailyStat(
date: asString(json['date']),
income: asDouble(json['income']),
expense: asDouble(json['expense']),
);
}