This commit is contained in:
2026-08-08 10:57:54 +08:00
parent d0d48c1ffc
commit 604af909b0
184 changed files with 12521 additions and 12722 deletions
+135
View File
@@ -0,0 +1,135 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import '../../../core/providers/app_providers.dart';
import '../../../data/local/models/models.dart';
import '../../scoring/application/scoring_service.dart';
/// Pure-data summary of local archery statistics, derived from completed
/// [PointRecord]s. All display strings are localized in zh-CN.
class StatsSummary {
const StatsSummary({
this.todayArrowCount = 0,
this.trainingDays = 0,
this.totalArrowCount = 0,
this.averageScore,
this.scoreDistribution = const <ScoreBucket>[],
this.hitPoints = const <HitPoint>[],
});
final int todayArrowCount;
final int trainingDays;
final int totalArrowCount;
final double? averageScore;
final List<ScoreBucket> scoreDistribution;
final List<HitPoint> hitPoints;
bool get hasData => totalArrowCount > 0;
double get todayCalories => todayArrowCount * 1.6;
/// Capped at 10 per PRD: todayArrows * 5 / 60.
double get todayIntensityRaw {
final v = todayArrowCount * 5 / 60.0;
return v > 10 ? 10 : v;
}
String get todayIntensityLabel {
final v = todayIntensityRaw;
if (v > 6) return '重度';
if (v >= 4) return '中度';
return '轻度';
}
String get averageScoreLabel =>
averageScore == null ? '-' : averageScore!.toStringAsFixed(1);
}
/// One bucket in the ring-value distribution. [label] follows the
/// "M / X / 1~10" convention from the PRD.
class ScoreBucket {
const ScoreBucket({required this.label, required this.count});
final String label;
final int count;
}
final statsProvider = FutureProvider<StatsSummary>((ref) async {
// Re-compute whenever the records list changes.
ref.watch(pointRecordsProvider);
final isar = await ref.watch(isarProvider.future);
final records = await isar.pointRecords
.filter()
.statusEqualTo(PointRecordStatus.completed)
.sortByCreatedAtDesc()
.findAll();
return _compute(records);
});
StatsSummary _compute(List<PointRecord> records) {
if (records.isEmpty) return const StatsSummary();
final today = DateTime.now();
final todayDate = DateTime(today.year, today.month, today.day);
int todayArrows = 0;
int totalArrows = 0;
double scoreSum = 0;
int scoredArrows = 0;
final days = <DateTime>{};
final allHits = <HitPoint>[];
final counts = <String, int>{};
for (final r in records) {
final day = DateTime(r.createdAt.year, r.createdAt.month, r.createdAt.day);
days.add(day);
totalArrows += r.scores.length;
if (day == todayDate) {
todayArrows += r.scores.length;
}
for (final s in r.scores) {
scoreSum += s;
scoredArrows += 1;
final key = _bucketLabel(s);
counts[key] = (counts[key] ?? 0) + 1;
}
allHits.addAll(r.hitPoints);
}
final order = const [
'X',
'10',
'9',
'8',
'7',
'6',
'5',
'4',
'3',
'2',
'1',
'M',
];
final distribution = <ScoreBucket>[];
for (final label in order) {
final c = counts[label] ?? 0;
if (c > 0) distribution.add(ScoreBucket(label: label, count: c));
}
return StatsSummary(
todayArrowCount: todayArrows,
trainingDays: days.length,
totalArrowCount: totalArrows,
averageScore: scoredArrows == 0 ? null : scoreSum / scoredArrows,
scoreDistribution: distribution,
hitPoints: allHits,
);
}
String _bucketLabel(int score) {
if (score == 0) return 'M';
if (score == 10) return '10';
return '$score';
}