91 lines
2.8 KiB
Dart
91 lines
2.8 KiB
Dart
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']),
|
||
);
|
||
}
|