mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-16 13:10:28 +00:00
feat: YearlyReportUI Update
This commit is contained in:
parent
6ec973144e
commit
70c47a8b85
@ -45,6 +45,20 @@ class YearlyReportData {
|
|||||||
final int yearlyDeathCount; // 年度死亡次数
|
final int yearlyDeathCount; // 年度死亡次数
|
||||||
final int yearlySelfKillCount; // 年度自杀次数
|
final int yearlySelfKillCount; // 年度自杀次数
|
||||||
|
|
||||||
|
// 月份统计
|
||||||
|
final int? mostPlayedMonth; // 游玩最多的月份 (1-12)
|
||||||
|
final int mostPlayedMonthCount; // 该月游玩次数
|
||||||
|
final int? leastPlayedMonth; // 游玩最少的月份 (1-12, 不包括完全没上游戏的月份)
|
||||||
|
final int leastPlayedMonthCount; // 该月游玩次数
|
||||||
|
|
||||||
|
// 连续游玩/离线统计
|
||||||
|
final int longestPlayStreak; // 最长连续游玩天数
|
||||||
|
final DateTime? playStreakStartDate; // 连续游玩开始日期
|
||||||
|
final DateTime? playStreakEndDate; // 连续游玩结束日期
|
||||||
|
final int longestOfflineStreak; // 最长连续离线天数
|
||||||
|
final DateTime? offlineStreakStartDate; // 连续离线开始日期
|
||||||
|
final DateTime? offlineStreakEndDate; // 连续离线结束日期
|
||||||
|
|
||||||
// 详细数据 (用于展示)
|
// 详细数据 (用于展示)
|
||||||
final Map<String, int> vehiclePilotedDetails; // 驾驶载具详情
|
final Map<String, int> vehiclePilotedDetails; // 驾驶载具详情
|
||||||
final Map<String, int> accountSessionDetails; // 账号会话详情
|
final Map<String, int> accountSessionDetails; // 账号会话详情
|
||||||
@ -77,6 +91,16 @@ class YearlyReportData {
|
|||||||
required this.yearlyKillCount,
|
required this.yearlyKillCount,
|
||||||
required this.yearlyDeathCount,
|
required this.yearlyDeathCount,
|
||||||
required this.yearlySelfKillCount,
|
required this.yearlySelfKillCount,
|
||||||
|
this.mostPlayedMonth,
|
||||||
|
required this.mostPlayedMonthCount,
|
||||||
|
this.leastPlayedMonth,
|
||||||
|
required this.leastPlayedMonthCount,
|
||||||
|
required this.longestPlayStreak,
|
||||||
|
this.playStreakStartDate,
|
||||||
|
this.playStreakEndDate,
|
||||||
|
required this.longestOfflineStreak,
|
||||||
|
this.offlineStreakStartDate,
|
||||||
|
this.offlineStreakEndDate,
|
||||||
required this.vehiclePilotedDetails,
|
required this.vehiclePilotedDetails,
|
||||||
required this.accountSessionDetails,
|
required this.accountSessionDetails,
|
||||||
required this.locationDetails,
|
required this.locationDetails,
|
||||||
@ -140,6 +164,20 @@ class YearlyReportData {
|
|||||||
'yearlyDeathCount': yearlyDeathCount,
|
'yearlyDeathCount': yearlyDeathCount,
|
||||||
'yearlySelfKillCount': yearlySelfKillCount,
|
'yearlySelfKillCount': yearlySelfKillCount,
|
||||||
|
|
||||||
|
// 月份统计
|
||||||
|
'mostPlayedMonth': mostPlayedMonth,
|
||||||
|
'mostPlayedMonthCount': mostPlayedMonthCount,
|
||||||
|
'leastPlayedMonth': leastPlayedMonth,
|
||||||
|
'leastPlayedMonthCount': leastPlayedMonthCount,
|
||||||
|
|
||||||
|
// 连续游玩/离线统计
|
||||||
|
'longestPlayStreak': longestPlayStreak,
|
||||||
|
'playStreakStartDateUtc': _toUtcTimestamp(playStreakStartDate),
|
||||||
|
'playStreakEndDateUtc': _toUtcTimestamp(playStreakEndDate),
|
||||||
|
'longestOfflineStreak': longestOfflineStreak,
|
||||||
|
'offlineStreakStartDateUtc': _toUtcTimestamp(offlineStreakStartDate),
|
||||||
|
'offlineStreakEndDateUtc': _toUtcTimestamp(offlineStreakEndDate),
|
||||||
|
|
||||||
// 详细数据
|
// 详细数据
|
||||||
'vehiclePilotedDetails': vehiclePilotedDetails,
|
'vehiclePilotedDetails': vehiclePilotedDetails,
|
||||||
'accountSessionDetails': accountSessionDetails,
|
'accountSessionDetails': accountSessionDetails,
|
||||||
@ -193,6 +231,9 @@ class _LogFileStats {
|
|||||||
// 地点访问: 地点名 -> 次数
|
// 地点访问: 地点名 -> 次数
|
||||||
Map<String, int> locationVisits = {};
|
Map<String, int> locationVisits = {};
|
||||||
|
|
||||||
|
// 上次记录死亡的时间 (用于 2s 内去重)
|
||||||
|
DateTime? _lastDeathTime;
|
||||||
|
|
||||||
// 年度内的会话记录
|
// 年度内的会话记录
|
||||||
List<_SessionInfo> yearlySessions = [];
|
List<_SessionInfo> yearlySessions = [];
|
||||||
|
|
||||||
@ -279,9 +320,17 @@ class YearlyReportAnalyzer {
|
|||||||
nameMatch ??= _legacyCharacterNamePattern.firstMatch(line);
|
nameMatch ??= _legacyCharacterNamePattern.firstMatch(line);
|
||||||
if (nameMatch != null) {
|
if (nameMatch != null) {
|
||||||
final playerName = nameMatch.group(1)?.trim();
|
final playerName = nameMatch.group(1)?.trim();
|
||||||
if (playerName != null && playerName.isNotEmpty && !playerName.contains(' ')) {
|
if (playerName != null &&
|
||||||
|
playerName.isNotEmpty &&
|
||||||
|
!playerName.contains(' ') &&
|
||||||
|
!playerName.contains('/') &&
|
||||||
|
!playerName.contains(r'\') &&
|
||||||
|
!playerName.contains('.')) {
|
||||||
stats.currentPlayerName = playerName;
|
stats.currentPlayerName = playerName;
|
||||||
|
// 去重添加到玩家列表 (忽略大小写)
|
||||||
|
if (!stats.playerNames.any((n) => n.toLowerCase() == playerName.toLowerCase())) {
|
||||||
stats.playerNames.add(playerName);
|
stats.playerNames.add(playerName);
|
||||||
|
}
|
||||||
stats.firstPlayerName ??= playerName;
|
stats.firstPlayerName ??= playerName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -318,7 +367,11 @@ class YearlyReportAnalyzer {
|
|||||||
if (deathMatch != null) {
|
if (deathMatch != null) {
|
||||||
final victimId = deathMatch.group(1)?.trim();
|
final victimId = deathMatch.group(1)?.trim();
|
||||||
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
|
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
|
||||||
|
// 防抖去重 (2秒内不重复计数)
|
||||||
|
if (stats._lastDeathTime == null || lineTime.difference(stats._lastDeathTime!).abs().inSeconds > 2) {
|
||||||
stats.deathCount++;
|
stats.deathCount++;
|
||||||
|
stats._lastDeathTime = lineTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,17 +382,34 @@ class YearlyReportAnalyzer {
|
|||||||
final killerId = legacyDeathMatch.group(3)?.trim();
|
final killerId = legacyDeathMatch.group(3)?.trim();
|
||||||
|
|
||||||
if (victimId != null && stats.currentPlayerName != null) {
|
if (victimId != null && stats.currentPlayerName != null) {
|
||||||
|
bool isRecent =
|
||||||
|
stats._lastDeathTime != null && lineTime.difference(stats._lastDeathTime!).abs().inSeconds <= 2;
|
||||||
|
|
||||||
// 检测自杀
|
// 检测自杀
|
||||||
if (victimId == killerId) {
|
if (victimId == killerId) {
|
||||||
if (victimId == stats.currentPlayerName) {
|
if (victimId == stats.currentPlayerName) {
|
||||||
|
if (isRecent) {
|
||||||
|
// 如果最近已经记录过一次死亡 (可能是通用格式记录的),则修正为自杀
|
||||||
|
// 假设通用格式默认为 deathCount++,这里回退并加到 selfKillCount
|
||||||
|
if (stats.deathCount > 0) stats.deathCount--;
|
||||||
stats.selfKillCount++;
|
stats.selfKillCount++;
|
||||||
|
// 更新时间以保持锁定
|
||||||
|
stats._lastDeathTime = lineTime;
|
||||||
|
} else {
|
||||||
|
stats.selfKillCount++;
|
||||||
|
stats._lastDeathTime = lineTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 检测死亡
|
// 检测死亡 (被击杀)
|
||||||
if (victimId == stats.currentPlayerName) {
|
if (victimId == stats.currentPlayerName) {
|
||||||
|
// 如果最近已经记录过 (可能是通用格式),则认为是同一事件,忽略
|
||||||
|
if (!isRecent) {
|
||||||
stats.deathCount++;
|
stats.deathCount++;
|
||||||
|
stats._lastDeathTime = lineTime;
|
||||||
}
|
}
|
||||||
// 检测击杀
|
}
|
||||||
|
// 检测击杀 (杀别人)
|
||||||
if (killerId == stats.currentPlayerName) {
|
if (killerId == stats.currentPlayerName) {
|
||||||
stats.killCount++;
|
stats.killCount++;
|
||||||
}
|
}
|
||||||
@ -601,6 +671,90 @@ class YearlyReportAnalyzer {
|
|||||||
final sortedLocations = locationDetails.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
|
final sortedLocations = locationDetails.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
|
||||||
final topLocations = sortedLocations.take(10).toList();
|
final topLocations = sortedLocations.take(10).toList();
|
||||||
|
|
||||||
|
// 计算月份统计
|
||||||
|
final Map<int, int> monthlyPlayCount = {};
|
||||||
|
final Set<DateTime> playDates = {}; // 所有游玩的日期 (仅日期部分)
|
||||||
|
|
||||||
|
for (final stats in allStats) {
|
||||||
|
for (final session in stats.yearlySessions) {
|
||||||
|
final month = session.startTime.month;
|
||||||
|
monthlyPlayCount[month] = (monthlyPlayCount[month] ?? 0) + 1;
|
||||||
|
// 记录游玩日期 (只保留年月日)
|
||||||
|
playDates.add(DateTime(session.startTime.year, session.startTime.month, session.startTime.day));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int? mostPlayedMonth;
|
||||||
|
int mostPlayedMonthCount = 0;
|
||||||
|
int? leastPlayedMonth;
|
||||||
|
int leastPlayedMonthCount = 0;
|
||||||
|
|
||||||
|
if (monthlyPlayCount.isNotEmpty) {
|
||||||
|
// 最多游玩的月份
|
||||||
|
for (final entry in monthlyPlayCount.entries) {
|
||||||
|
if (entry.value > mostPlayedMonthCount) {
|
||||||
|
mostPlayedMonth = entry.key;
|
||||||
|
mostPlayedMonthCount = entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 最少游玩的月份 (不包括完全没上游戏的月份)
|
||||||
|
leastPlayedMonthCount = monthlyPlayCount.values.first;
|
||||||
|
for (final entry in monthlyPlayCount.entries) {
|
||||||
|
if (entry.value <= leastPlayedMonthCount) {
|
||||||
|
leastPlayedMonth = entry.key;
|
||||||
|
leastPlayedMonthCount = entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算连续游玩天数和连续离线天数
|
||||||
|
int longestPlayStreak = 0;
|
||||||
|
DateTime? playStreakStartDate;
|
||||||
|
DateTime? playStreakEndDate;
|
||||||
|
int longestOfflineStreak = 0;
|
||||||
|
DateTime? offlineStreakStartDate;
|
||||||
|
DateTime? offlineStreakEndDate;
|
||||||
|
|
||||||
|
if (playDates.isNotEmpty) {
|
||||||
|
// 将日期排序
|
||||||
|
final sortedDates = playDates.toList()..sort();
|
||||||
|
|
||||||
|
// 计算连续游玩天数
|
||||||
|
int currentStreak = 1;
|
||||||
|
DateTime streakStart = sortedDates.first;
|
||||||
|
|
||||||
|
for (int i = 1; i < sortedDates.length; i++) {
|
||||||
|
final diff = sortedDates[i].difference(sortedDates[i - 1]).inDays;
|
||||||
|
if (diff == 1) {
|
||||||
|
currentStreak++;
|
||||||
|
} else {
|
||||||
|
if (currentStreak > longestPlayStreak) {
|
||||||
|
longestPlayStreak = currentStreak;
|
||||||
|
playStreakStartDate = streakStart;
|
||||||
|
playStreakEndDate = sortedDates[i - 1];
|
||||||
|
}
|
||||||
|
currentStreak = 1;
|
||||||
|
streakStart = sortedDates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 检查最后一段连续
|
||||||
|
if (currentStreak > longestPlayStreak) {
|
||||||
|
longestPlayStreak = currentStreak;
|
||||||
|
playStreakStartDate = streakStart;
|
||||||
|
playStreakEndDate = sortedDates.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算连续离线天数 (在游玩日期之间的间隔)
|
||||||
|
for (int i = 1; i < sortedDates.length; i++) {
|
||||||
|
final gapDays = sortedDates[i].difference(sortedDates[i - 1]).inDays - 1;
|
||||||
|
if (gapDays > longestOfflineStreak) {
|
||||||
|
longestOfflineStreak = gapDays;
|
||||||
|
offlineStreakStartDate = sortedDates[i - 1].add(const Duration(days: 1));
|
||||||
|
offlineStreakEndDate = sortedDates[i].subtract(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return YearlyReportData(
|
return YearlyReportData(
|
||||||
totalLaunchCount: totalLaunchCount,
|
totalLaunchCount: totalLaunchCount,
|
||||||
totalPlayTime: totalPlayTime,
|
totalPlayTime: totalPlayTime,
|
||||||
@ -628,6 +782,16 @@ class YearlyReportAnalyzer {
|
|||||||
yearlyKillCount: yearlyKillCount,
|
yearlyKillCount: yearlyKillCount,
|
||||||
yearlyDeathCount: yearlyDeathCount,
|
yearlyDeathCount: yearlyDeathCount,
|
||||||
yearlySelfKillCount: yearlySelfKillCount,
|
yearlySelfKillCount: yearlySelfKillCount,
|
||||||
|
mostPlayedMonth: mostPlayedMonth,
|
||||||
|
mostPlayedMonthCount: mostPlayedMonthCount,
|
||||||
|
leastPlayedMonth: leastPlayedMonth,
|
||||||
|
leastPlayedMonthCount: leastPlayedMonthCount,
|
||||||
|
longestPlayStreak: longestPlayStreak,
|
||||||
|
playStreakStartDate: playStreakStartDate,
|
||||||
|
playStreakEndDate: playStreakEndDate,
|
||||||
|
longestOfflineStreak: longestOfflineStreak,
|
||||||
|
offlineStreakStartDate: offlineStreakStartDate,
|
||||||
|
offlineStreakEndDate: offlineStreakEndDate,
|
||||||
vehiclePilotedDetails: vehiclePilotedDetails,
|
vehiclePilotedDetails: vehiclePilotedDetails,
|
||||||
accountSessionDetails: accountSessionDetails,
|
accountSessionDetails: accountSessionDetails,
|
||||||
locationDetails: locationDetails,
|
locationDetails: locationDetails,
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
|
|
||||||
return makeDefaultPage(
|
return makeDefaultPage(
|
||||||
context,
|
context,
|
||||||
title: "2025 年度报告",
|
title: "星际公民 2025 年度报告",
|
||||||
useBodyContainer: true,
|
useBodyContainer: true,
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -233,6 +233,10 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
_buildPlayTimePage(context, data),
|
_buildPlayTimePage(context, data),
|
||||||
// 游玩时长详情
|
// 游玩时长详情
|
||||||
_buildSessionStatsPage(context, data),
|
_buildSessionStatsPage(context, data),
|
||||||
|
// 月份统计
|
||||||
|
_buildMonthlyStatsPage(context, data),
|
||||||
|
// 连续游玩/离线统计
|
||||||
|
_buildStreakStatsPage(context, data),
|
||||||
// 崩溃次数
|
// 崩溃次数
|
||||||
_buildCrashCountPage(context, data),
|
_buildCrashCountPage(context, data),
|
||||||
// 击杀统计 (K/D)
|
// 击杀统计 (K/D)
|
||||||
@ -375,13 +379,15 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
// 详细数据
|
// 详细数据 - 等宽卡片
|
||||||
FadeInUp(
|
FadeInUp(
|
||||||
delay: const Duration(milliseconds: 600),
|
delay: const Duration(milliseconds: 600),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).cardColor,
|
color: FluentTheme.of(context).cardColor,
|
||||||
@ -400,8 +406,11 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Container(
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).cardColor,
|
color: FluentTheme.of(context).cardColor,
|
||||||
@ -420,8 +429,11 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Container(
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).cardColor,
|
color: FluentTheme.of(context).cardColor,
|
||||||
@ -440,6 +452,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -653,15 +666,22 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
onPointerSignal: (event) {
|
onPointerSignal: (event) {
|
||||||
if (event is PointerScrollEvent) {
|
if (event is PointerScrollEvent) {
|
||||||
final newOffset = scrollController.offset + event.scrollDelta.dy;
|
final newOffset = scrollController.offset + event.scrollDelta.dy;
|
||||||
if (newOffset >= scrollController.position.minScrollExtent &&
|
final canScroll =
|
||||||
newOffset <= scrollController.position.maxScrollExtent) {
|
newOffset >= scrollController.position.minScrollExtent &&
|
||||||
|
newOffset <= scrollController.position.maxScrollExtent;
|
||||||
|
if (canScroll) {
|
||||||
scrollController.jumpTo(newOffset);
|
scrollController.jumpTo(newOffset);
|
||||||
}
|
}
|
||||||
|
// 消费滚动事件,阻止向上冒泡触发页面滚动
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (_) => true, // 消费所有滚动通知
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: sortedVehicles.map((vehicle) {
|
children: sortedVehicles.map((vehicle) {
|
||||||
@ -705,6 +725,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -873,102 +894,111 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
child: Text("游玩时长详情", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
child: Text("游玩时长详情", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
// 平均时长
|
// 横排显示三个时长卡片
|
||||||
FadeInUp(
|
FadeInUp(
|
||||||
delay: const Duration(milliseconds: 400),
|
delay: const Duration(milliseconds: 400),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 平均时长
|
||||||
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(16),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(FontAwesomeIcons.chartLine, size: 20, color: Colors.blue),
|
Icon(FontAwesomeIcons.chartLine, size: 16, color: Colors.blue),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
Text("平均每次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
|
Text("平均", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_formatDuration(data.averageSessionTime),
|
_formatDuration(data.averageSessionTime),
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(width: 12),
|
||||||
// 最长游玩
|
// 最长游玩
|
||||||
FadeInUp(
|
Flexible(
|
||||||
delay: const Duration(milliseconds: 600),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(16),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(FontAwesomeIcons.arrowUp, size: 20, color: Colors.green),
|
Icon(FontAwesomeIcons.arrowUp, size: 16, color: Colors.green),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
Text("最长一次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
|
Text("最长", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_formatDuration(data.longestSession),
|
_formatDuration(data.longestSession),
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.green),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
if (data.longestSessionDate != null)
|
if (data.longestSessionDate != null)
|
||||||
Text(
|
Text(
|
||||||
"${data.longestSessionDate!.month}月${data.longestSessionDate!.day}日",
|
"${data.longestSessionDate!.month}月${data.longestSessionDate!.day}日",
|
||||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(width: 12),
|
||||||
// 最短游玩
|
// 最短游玩
|
||||||
FadeInUp(
|
Flexible(
|
||||||
delay: const Duration(milliseconds: 800),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(16),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(FontAwesomeIcons.arrowDown, size: 20, color: Colors.orange),
|
Icon(FontAwesomeIcons.arrowDown, size: 16, color: Colors.orange),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
Text("最短一次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
|
Text("最短", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_formatDuration(data.shortestSession),
|
_formatDuration(data.shortestSession),
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.orange),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.orange),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
if (data.shortestSessionDate != null)
|
if (data.shortestSessionDate != null)
|
||||||
Text(
|
Text(
|
||||||
"${data.shortestSessionDate!.month}月${data.shortestSessionDate!.day}日",
|
"${data.shortestSessionDate!.month}月${data.shortestSessionDate!.day}日",
|
||||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
|
||||||
),
|
),
|
||||||
Text("(仅统计超过 5 分钟的游戏)", style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6))),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -976,10 +1006,233 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FadeInUp(
|
||||||
|
delay: const Duration(milliseconds: 600),
|
||||||
|
child: Text(
|
||||||
|
"(最短仅统计超过 5 分钟的游戏)",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 月份名称
|
||||||
|
String _getMonthName(int month) {
|
||||||
|
return "$month月";
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMonthlyStatsPage(BuildContext context, YearlyReportData data) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ZoomIn(child: Icon(FontAwesomeIcons.calendarDays, size: 64, color: Colors.blue)),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
FadeInUp(
|
||||||
|
delay: const Duration(milliseconds: 200),
|
||||||
|
child: Text("月份统计", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
// 并排展示
|
||||||
|
FadeInUp(
|
||||||
|
delay: const Duration(milliseconds: 400),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 游玩最多的月份
|
||||||
|
if (data.mostPlayedMonth != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.fire, size: 18, color: Colors.orange),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text("游玩最多", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_getMonthName(data.mostPlayedMonth!),
|
||||||
|
style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.orange),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"启动了 ${data.mostPlayedMonthCount} 次",
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 游玩最少的月份
|
||||||
|
if (data.leastPlayedMonth != null && data.leastPlayedMonth != data.mostPlayedMonth)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.snowflake, size: 18, color: Colors.teal),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text("游玩最少", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_getMonthName(data.leastPlayedMonth!),
|
||||||
|
style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.teal),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"仅启动 ${data.leastPlayedMonthCount} 次",
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStreakStatsPage(BuildContext context, YearlyReportData data) {
|
||||||
|
String formatDateRange(DateTime? start, DateTime? end) {
|
||||||
|
if (start == null || end == null) return "";
|
||||||
|
return "${start.month}月${start.day}日 - ${end.month}月${end.day}日";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ZoomIn(child: Icon(FontAwesomeIcons.fire, size: 64, color: Colors.red)),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
FadeInUp(
|
||||||
|
delay: const Duration(milliseconds: 200),
|
||||||
|
child: Text("连续记录", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
// 并排展示
|
||||||
|
FadeInUp(
|
||||||
|
delay: const Duration(milliseconds: 400),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 连续游玩天数
|
||||||
|
if (data.longestPlayStreak > 0)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.gamepad, size: 18, color: Colors.green),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text("连续游玩", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${data.longestPlayStreak}",
|
||||||
|
style: TextStyle(fontSize: 42, fontWeight: FontWeight.bold, color: Colors.green),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6, left: 4),
|
||||||
|
child: Text("天", style: TextStyle(fontSize: 18, color: Colors.green)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (data.playStreakStartDate != null)
|
||||||
|
Text(
|
||||||
|
formatDateRange(data.playStreakStartDate, data.playStreakEndDate),
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 连续离线天数
|
||||||
|
if (data.longestOfflineStreak > 0)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.bed, size: 18, color: Colors.grey),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text("连续离线", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${data.longestOfflineStreak}",
|
||||||
|
style: TextStyle(fontSize: 42, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6, left: 4),
|
||||||
|
child: Text("天", style: TextStyle(fontSize: 18, color: Colors.grey)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (data.offlineStreakStartDate != null)
|
||||||
|
Text(
|
||||||
|
formatDateRange(data.offlineStreakStartDate, data.offlineStreakEndDate),
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLocationStatsPage(BuildContext context, YearlyReportData data) {
|
Widget _buildLocationStatsPage(BuildContext context, YearlyReportData data) {
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
|
||||||
if (data.topLocations.isEmpty) {
|
if (data.topLocations.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -1001,9 +1254,15 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将地点分成3行
|
||||||
|
final locations = data.topLocations;
|
||||||
|
final rowCount = 3;
|
||||||
|
final List<List<MapEntry<String, int>>> rows = List.generate(rowCount, (_) => []);
|
||||||
|
for (int i = 0; i < locations.length; i++) {
|
||||||
|
rows[i % rowCount].add(locations[i]);
|
||||||
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -1019,59 +1278,113 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
child: Text("基于库存查看记录统计", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6))),
|
child: Text("基于库存查看记录统计", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6))),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Top 地点列表
|
// 三排瀑布流横向滑动
|
||||||
...data.topLocations.asMap().entries.map((entry) {
|
FadeInUp(
|
||||||
final index = entry.key;
|
delay: const Duration(milliseconds: 400),
|
||||||
final location = entry.value;
|
child: SizedBox(
|
||||||
final isTop3 = index < 3;
|
height: 180,
|
||||||
|
width: double.infinity,
|
||||||
return FadeInUp(
|
child: GestureDetector(
|
||||||
delay: Duration(milliseconds: 400 + index * 100),
|
onVerticalDragUpdate: (_) {}, // 拦截垂直拖拽
|
||||||
child: Container(
|
child: Listener(
|
||||||
width: 350,
|
onPointerSignal: (event) {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
if (event is PointerScrollEvent) {
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
// 注册为唯一的滚轮事件处理者,阻止冒泡
|
||||||
|
GestureBinding.instance.pointerSignalResolver.register(event, (event) {
|
||||||
|
final scrollEvent = event as PointerScrollEvent;
|
||||||
|
final newOffset = scrollController.offset + scrollEvent.scrollDelta.dy;
|
||||||
|
final clampedOffset = newOffset.clamp(
|
||||||
|
scrollController.position.minScrollExtent,
|
||||||
|
scrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
scrollController.jumpTo(clampedOffset);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: rows.asMap().entries.map((rowEntry) {
|
||||||
|
final rowIndex = rowEntry.key;
|
||||||
|
final rowLocations = rowEntry.value;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: rowLocations.asMap().entries.map((locEntry) {
|
||||||
|
final actualIndex = locEntry.key * rowCount + rowIndex;
|
||||||
|
final location = locEntry.value;
|
||||||
|
final isTop3 = actualIndex < 3;
|
||||||
|
return Container(
|
||||||
|
width: 200,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isTop3
|
color: isTop3
|
||||||
? FluentTheme.of(context).accentColor.withValues(alpha: .2 - index * 0.05)
|
? FluentTheme.of(context).accentColor.withValues(alpha: .2 - actualIndex * 0.05)
|
||||||
: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 28,
|
width: 22,
|
||||||
height: 28,
|
height: 22,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isTop3 ? Colors.yellow.withValues(alpha: 1 - index * 0.3) : Colors.grey,
|
color: isTop3
|
||||||
|
? Colors.yellow.withValues(alpha: 1 - actualIndex * 0.3)
|
||||||
|
: Colors.grey,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${index + 1}",
|
"${actualIndex + 1}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isTop3 ? Colors.black : Colors.white,
|
color: isTop3 ? Colors.black : Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(location.key, style: TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis),
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
location.key,
|
||||||
|
style: TextStyle(fontSize: 11),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${location.value} 次",
|
"${location.value} 次",
|
||||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
|
style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .5)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}).toList(),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1108,21 +1421,36 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildDataSummaryPage(BuildContext context, YearlyReportData data) {
|
Widget _buildDataSummaryPage(BuildContext context, YearlyReportData data) {
|
||||||
final yearlyHours = data.yearlyPlayTime.inMinutes / 60;
|
final yearlyHours = data.yearlyPlayTime.inMinutes / 60;
|
||||||
final kdRatio = data.yearlyDeathCount > 0
|
|
||||||
? (data.yearlyKillCount / data.yearlyDeathCount).toStringAsFixed(2)
|
|
||||||
: data.yearlyKillCount > 0
|
|
||||||
? "∞"
|
|
||||||
: "-";
|
|
||||||
|
|
||||||
// 构建数据项列表
|
// 构建数据项列表
|
||||||
final dataItems = <_SummaryGridItem>[
|
final dataItems = <_SummaryGridItem>[
|
||||||
_SummaryGridItem("启动游戏", "${data.yearlyLaunchCount}", "次", FontAwesomeIcons.play, Colors.green),
|
_SummaryGridItem("启动游戏", "${data.yearlyLaunchCount}", "次", FontAwesomeIcons.play, Colors.green, isWide: false),
|
||||||
_SummaryGridItem("游玩时长", yearlyHours.toStringAsFixed(1), "小时", FontAwesomeIcons.clock, Colors.blue),
|
_SummaryGridItem(
|
||||||
_SummaryGridItem("游戏崩溃", "${data.yearlyCrashCount}", "次", FontAwesomeIcons.bug, Colors.orange),
|
"游玩时长",
|
||||||
_SummaryGridItem("击杀玩家", "${data.yearlyKillCount}", "次", FontAwesomeIcons.crosshairs, Colors.green),
|
yearlyHours.toStringAsFixed(1),
|
||||||
_SummaryGridItem("意外死亡", "${data.yearlyDeathCount}", "次", FontAwesomeIcons.skull, Colors.red),
|
"小时",
|
||||||
_SummaryGridItem("KD 比率", kdRatio, "", FontAwesomeIcons.chartLine, Colors.teal),
|
FontAwesomeIcons.clock,
|
||||||
_SummaryGridItem("载具损毁", "${data.yearlyVehicleDestructionCount}", "次", FontAwesomeIcons.explosion, Colors.red),
|
Colors.blue,
|
||||||
|
isWide: false,
|
||||||
|
),
|
||||||
|
_SummaryGridItem("游戏崩溃", "${data.yearlyCrashCount}", "次", FontAwesomeIcons.bug, Colors.orange, isWide: false),
|
||||||
|
_SummaryGridItem(
|
||||||
|
"击杀",
|
||||||
|
"${data.yearlyKillCount}",
|
||||||
|
"次",
|
||||||
|
FontAwesomeIcons.crosshairs,
|
||||||
|
Colors.green,
|
||||||
|
isWide: false,
|
||||||
|
),
|
||||||
|
_SummaryGridItem("死亡", "${data.yearlyDeathCount}", "次", FontAwesomeIcons.skull, Colors.red, isWide: false),
|
||||||
|
_SummaryGridItem(
|
||||||
|
"载具损毁",
|
||||||
|
"${data.yearlyVehicleDestructionCount}",
|
||||||
|
"次",
|
||||||
|
FontAwesomeIcons.explosion,
|
||||||
|
Colors.red,
|
||||||
|
isWide: false,
|
||||||
|
),
|
||||||
if (data.longestSession != null)
|
if (data.longestSession != null)
|
||||||
_SummaryGridItem(
|
_SummaryGridItem(
|
||||||
"最长在线",
|
"最长在线",
|
||||||
@ -1130,17 +1458,9 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
"小时",
|
"小时",
|
||||||
FontAwesomeIcons.hourglassHalf,
|
FontAwesomeIcons.hourglassHalf,
|
||||||
Colors.purple,
|
Colors.purple,
|
||||||
|
isWide: false,
|
||||||
),
|
),
|
||||||
if (data.topLocations.isNotEmpty)
|
// 常去位置单独处理,不放在网格中
|
||||||
_SummaryGridItem(
|
|
||||||
"最常去",
|
|
||||||
data.topLocations.first.key.length > 6
|
|
||||||
? "${data.topLocations.first.key.substring(0, 5)}..."
|
|
||||||
: data.topLocations.first.key,
|
|
||||||
"",
|
|
||||||
FontAwesomeIcons.locationDot,
|
|
||||||
Colors.grey,
|
|
||||||
),
|
|
||||||
if (data.earliestPlayDate != null)
|
if (data.earliestPlayDate != null)
|
||||||
_SummaryGridItem(
|
_SummaryGridItem(
|
||||||
"最早时刻",
|
"最早时刻",
|
||||||
@ -1148,6 +1468,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
"",
|
"",
|
||||||
FontAwesomeIcons.sun,
|
FontAwesomeIcons.sun,
|
||||||
Colors.orange,
|
Colors.orange,
|
||||||
|
isWide: false,
|
||||||
),
|
),
|
||||||
if (data.latestPlayDate != null)
|
if (data.latestPlayDate != null)
|
||||||
_SummaryGridItem(
|
_SummaryGridItem(
|
||||||
@ -1156,8 +1477,57 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
"",
|
"",
|
||||||
FontAwesomeIcons.moon,
|
FontAwesomeIcons.moon,
|
||||||
Colors.purple,
|
Colors.purple,
|
||||||
|
isWide: false,
|
||||||
|
),
|
||||||
|
_SummaryGridItem(
|
||||||
|
"重开次数",
|
||||||
|
"${data.yearlySelfKillCount}",
|
||||||
|
"次",
|
||||||
|
FontAwesomeIcons.personFalling,
|
||||||
|
Colors.grey,
|
||||||
|
isWide: false,
|
||||||
|
),
|
||||||
|
// 月份统计
|
||||||
|
if (data.mostPlayedMonth != null)
|
||||||
|
_SummaryGridItem(
|
||||||
|
"最热月",
|
||||||
|
_getMonthName(data.mostPlayedMonth!),
|
||||||
|
"",
|
||||||
|
FontAwesomeIcons.fire,
|
||||||
|
Colors.orange,
|
||||||
|
isWide: false,
|
||||||
|
),
|
||||||
|
// 连续游玩/离线
|
||||||
|
if (data.longestPlayStreak > 0)
|
||||||
|
_SummaryGridItem(
|
||||||
|
"连续游玩",
|
||||||
|
"${data.longestPlayStreak}",
|
||||||
|
"天",
|
||||||
|
FontAwesomeIcons.gamepad,
|
||||||
|
Colors.green,
|
||||||
|
isWide: false,
|
||||||
|
),
|
||||||
|
if (data.longestOfflineStreak > 0)
|
||||||
|
_SummaryGridItem("连续离线", "${data.longestOfflineStreak}", "天", FontAwesomeIcons.bed, Colors.grey, isWide: false),
|
||||||
|
// 常去位置和最爱载具
|
||||||
|
if (data.topLocations.isNotEmpty)
|
||||||
|
_SummaryGridItem(
|
||||||
|
"常去位置",
|
||||||
|
data.topLocations.first.key,
|
||||||
|
"",
|
||||||
|
FontAwesomeIcons.locationDot,
|
||||||
|
Colors.red,
|
||||||
|
isWide: true, // 使用较小字体
|
||||||
|
),
|
||||||
|
if (data.mostPilotedVehicle != null)
|
||||||
|
_SummaryGridItem(
|
||||||
|
"最爱载具",
|
||||||
|
data.mostPilotedVehicle!,
|
||||||
|
"",
|
||||||
|
FontAwesomeIcons.shuttleSpace,
|
||||||
|
Colors.teal,
|
||||||
|
isWide: true, // 使用较小字体
|
||||||
),
|
),
|
||||||
_SummaryGridItem("重开次数", "${data.yearlySelfKillCount}", "次", FontAwesomeIcons.personFalling, Colors.grey),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
@ -1173,7 +1543,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
Icon(FontAwesomeIcons.star, size: 20, color: Colors.yellow),
|
Icon(FontAwesomeIcons.star, size: 20, color: Colors.yellow),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
"2025 年度报告",
|
"星际公民 2025 年度报告",
|
||||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@ -1195,19 +1565,21 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
// 数据网格
|
// 数据网格
|
||||||
FadeInUp(
|
FadeInUp(
|
||||||
delay: const Duration(milliseconds: 200),
|
delay: const Duration(milliseconds: 200),
|
||||||
child: Container(
|
child: Padding(
|
||||||
constraints: const BoxConstraints(maxWidth: 400),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
child: MasonryGridView.count(
|
child: MasonryGridView.count(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 5,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 12,
|
||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 12,
|
||||||
itemCount: dataItems.length,
|
itemCount: dataItems.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = dataItems[index];
|
final item = dataItems[index];
|
||||||
|
final isSmallFont = item.isWide; // 载具和位置使用较小字体
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
height: 150,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@ -1216,8 +1588,8 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(item.icon, size: 14, color: item.color.withValues(alpha: .8)),
|
Icon(item.icon, size: 20, color: item.color.withValues(alpha: .8)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
@ -1226,30 +1598,32 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
item.value,
|
item.value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 34,
|
fontSize: isSmallFont ? 16 : 42,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: isSmallFont ? 2 : null,
|
||||||
|
overflow: isSmallFont ? TextOverflow.ellipsis : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (item.unit.isNotEmpty) ...[
|
if (item.unit.isNotEmpty) ...[
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 4),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
item.unit,
|
item.unit,
|
||||||
style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .6)),
|
style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .6)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
item.label,
|
item.label,
|
||||||
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .5)),
|
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .5)),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1259,37 +1633,13 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 20),
|
||||||
// 最爱载具
|
|
||||||
if (data.mostPilotedVehicle != null)
|
|
||||||
FadeInUp(
|
|
||||||
delay: const Duration(milliseconds: 400),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.shuttleSpace, size: 14, color: Colors.teal.withValues(alpha: .7)),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(
|
|
||||||
"最爱: ${data.mostPilotedVehicle}",
|
|
||||||
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .8)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 底部标识
|
// 底部标识
|
||||||
FadeInUp(
|
FadeInUp(
|
||||||
delay: const Duration(milliseconds: 600),
|
delay: const Duration(milliseconds: 400),
|
||||||
child: Text(
|
child: Text(
|
||||||
"SC汉化盒子 · 2025年度报告",
|
"由 SC 汉化盒子为您呈现",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .3)),
|
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .3)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1302,11 +1652,11 @@ class YearlyReportUI extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
|
||||||
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .3)),
|
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .15)),
|
||||||
child: Text(
|
child: Text(
|
||||||
"数据使用您的本地日志生成,不会发送到任何第三方。因跨版本 Log 改动较大,数据可能不完整,仅供娱乐。",
|
"数据使用您的本地日志生成,不会发送到任何第三方。因跨版本 Log 改动较大,数据可能不完整,仅供娱乐。",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .5)),
|
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .7)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1319,8 +1669,9 @@ class _SummaryGridItem {
|
|||||||
final String unit;
|
final String unit;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final bool isWide;
|
||||||
|
|
||||||
const _SummaryGridItem(this.label, this.value, this.unit, this.icon, this.color);
|
const _SummaryGridItem(this.label, this.value, this.unit, this.icon, this.color, {this.isWide = false});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 动画统计页面
|
/// 动画统计页面
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user