mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-18 06:00: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 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> accountSessionDetails; // 账号会话详情
|
||||
@ -77,6 +91,16 @@ class YearlyReportData {
|
||||
required this.yearlyKillCount,
|
||||
required this.yearlyDeathCount,
|
||||
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.accountSessionDetails,
|
||||
required this.locationDetails,
|
||||
@ -140,6 +164,20 @@ class YearlyReportData {
|
||||
'yearlyDeathCount': yearlyDeathCount,
|
||||
'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,
|
||||
'accountSessionDetails': accountSessionDetails,
|
||||
@ -193,6 +231,9 @@ class _LogFileStats {
|
||||
// 地点访问: 地点名 -> 次数
|
||||
Map<String, int> locationVisits = {};
|
||||
|
||||
// 上次记录死亡的时间 (用于 2s 内去重)
|
||||
DateTime? _lastDeathTime;
|
||||
|
||||
// 年度内的会话记录
|
||||
List<_SessionInfo> yearlySessions = [];
|
||||
|
||||
@ -279,9 +320,17 @@ class YearlyReportAnalyzer {
|
||||
nameMatch ??= _legacyCharacterNamePattern.firstMatch(line);
|
||||
if (nameMatch != null) {
|
||||
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;
|
||||
// 去重添加到玩家列表 (忽略大小写)
|
||||
if (!stats.playerNames.any((n) => n.toLowerCase() == playerName.toLowerCase())) {
|
||||
stats.playerNames.add(playerName);
|
||||
}
|
||||
stats.firstPlayerName ??= playerName;
|
||||
}
|
||||
}
|
||||
@ -318,7 +367,11 @@ class YearlyReportAnalyzer {
|
||||
if (deathMatch != null) {
|
||||
final victimId = deathMatch.group(1)?.trim();
|
||||
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
|
||||
// 防抖去重 (2秒内不重复计数)
|
||||
if (stats._lastDeathTime == null || lineTime.difference(stats._lastDeathTime!).abs().inSeconds > 2) {
|
||||
stats.deathCount++;
|
||||
stats._lastDeathTime = lineTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,17 +382,34 @@ class YearlyReportAnalyzer {
|
||||
final killerId = legacyDeathMatch.group(3)?.trim();
|
||||
|
||||
if (victimId != null && stats.currentPlayerName != null) {
|
||||
bool isRecent =
|
||||
stats._lastDeathTime != null && lineTime.difference(stats._lastDeathTime!).abs().inSeconds <= 2;
|
||||
|
||||
// 检测自杀
|
||||
if (victimId == killerId) {
|
||||
if (victimId == stats.currentPlayerName) {
|
||||
if (isRecent) {
|
||||
// 如果最近已经记录过一次死亡 (可能是通用格式记录的),则修正为自杀
|
||||
// 假设通用格式默认为 deathCount++,这里回退并加到 selfKillCount
|
||||
if (stats.deathCount > 0) stats.deathCount--;
|
||||
stats.selfKillCount++;
|
||||
// 更新时间以保持锁定
|
||||
stats._lastDeathTime = lineTime;
|
||||
} else {
|
||||
stats.selfKillCount++;
|
||||
stats._lastDeathTime = lineTime;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检测死亡
|
||||
// 检测死亡 (被击杀)
|
||||
if (victimId == stats.currentPlayerName) {
|
||||
// 如果最近已经记录过 (可能是通用格式),则认为是同一事件,忽略
|
||||
if (!isRecent) {
|
||||
stats.deathCount++;
|
||||
stats._lastDeathTime = lineTime;
|
||||
}
|
||||
// 检测击杀
|
||||
}
|
||||
// 检测击杀 (杀别人)
|
||||
if (killerId == stats.currentPlayerName) {
|
||||
stats.killCount++;
|
||||
}
|
||||
@ -601,6 +671,90 @@ class YearlyReportAnalyzer {
|
||||
final sortedLocations = locationDetails.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
|
||||
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(
|
||||
totalLaunchCount: totalLaunchCount,
|
||||
totalPlayTime: totalPlayTime,
|
||||
@ -628,6 +782,16 @@ class YearlyReportAnalyzer {
|
||||
yearlyKillCount: yearlyKillCount,
|
||||
yearlyDeathCount: yearlyDeathCount,
|
||||
yearlySelfKillCount: yearlySelfKillCount,
|
||||
mostPlayedMonth: mostPlayedMonth,
|
||||
mostPlayedMonthCount: mostPlayedMonthCount,
|
||||
leastPlayedMonth: leastPlayedMonth,
|
||||
leastPlayedMonthCount: leastPlayedMonthCount,
|
||||
longestPlayStreak: longestPlayStreak,
|
||||
playStreakStartDate: playStreakStartDate,
|
||||
playStreakEndDate: playStreakEndDate,
|
||||
longestOfflineStreak: longestOfflineStreak,
|
||||
offlineStreakStartDate: offlineStreakStartDate,
|
||||
offlineStreakEndDate: offlineStreakEndDate,
|
||||
vehiclePilotedDetails: vehiclePilotedDetails,
|
||||
accountSessionDetails: accountSessionDetails,
|
||||
locationDetails: locationDetails,
|
||||
|
||||
@ -29,7 +29,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
|
||||
return makeDefaultPage(
|
||||
context,
|
||||
title: "2025 年度报告",
|
||||
title: "星际公民 2025 年度报告",
|
||||
useBodyContainer: true,
|
||||
content: Column(
|
||||
children: [
|
||||
@ -233,6 +233,10 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
_buildPlayTimePage(context, data),
|
||||
// 游玩时长详情
|
||||
_buildSessionStatsPage(context, data),
|
||||
// 月份统计
|
||||
_buildMonthlyStatsPage(context, data),
|
||||
// 连续游玩/离线统计
|
||||
_buildStreakStatsPage(context, data),
|
||||
// 崩溃次数
|
||||
_buildCrashCountPage(context, data),
|
||||
// 击杀统计 (K/D)
|
||||
@ -375,13 +379,15 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// 详细数据
|
||||
// 详细数据 - 等宽卡片
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
@ -400,8 +406,11 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
@ -420,8 +429,11 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
@ -440,6 +452,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -653,15 +666,22 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
onPointerSignal: (event) {
|
||||
if (event is PointerScrollEvent) {
|
||||
final newOffset = scrollController.offset + event.scrollDelta.dy;
|
||||
if (newOffset >= scrollController.position.minScrollExtent &&
|
||||
newOffset <= scrollController.position.maxScrollExtent) {
|
||||
final canScroll =
|
||||
newOffset >= scrollController.position.minScrollExtent &&
|
||||
newOffset <= scrollController.position.maxScrollExtent;
|
||||
if (canScroll) {
|
||||
scrollController.jumpTo(newOffset);
|
||||
}
|
||||
// 消费滚动事件,阻止向上冒泡触发页面滚动
|
||||
}
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => true, // 消费所有滚动通知
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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)),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// 平均时长
|
||||
// 横排显示三个时长卡片
|
||||
FadeInUp(
|
||||
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(
|
||||
padding: const EdgeInsets.all(20),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.chartLine, size: 20, color: Colors.blue),
|
||||
const SizedBox(width: 12),
|
||||
Text("平均每次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
|
||||
Icon(FontAwesomeIcons.chartLine, size: 16, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Text("平均", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_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(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.arrowUp, size: 20, color: Colors.green),
|
||||
const SizedBox(width: 12),
|
||||
Text("最长一次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
|
||||
Icon(FontAwesomeIcons.arrowUp, size: 16, color: Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
Text("最长", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_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)
|
||||
Text(
|
||||
"${data.longestSessionDate!.month} 月 ${data.longestSessionDate!.day} 日",
|
||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
||||
"${data.longestSessionDate!.month}月${data.longestSessionDate!.day}日",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(width: 12),
|
||||
// 最短游玩
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 800),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.arrowDown, size: 20, color: Colors.orange),
|
||||
const SizedBox(width: 12),
|
||||
Text("最短一次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
|
||||
Icon(FontAwesomeIcons.arrowDown, size: 16, color: Colors.orange),
|
||||
const SizedBox(width: 8),
|
||||
Text("最短", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_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)
|
||||
Text(
|
||||
"${data.shortestSessionDate!.month} 月 ${data.shortestSessionDate!.day} 日",
|
||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
|
||||
"${data.shortestSessionDate!.month}月${data.shortestSessionDate!.day}日",
|
||||
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) {
|
||||
final scrollController = ScrollController();
|
||||
|
||||
if (data.topLocations.isEmpty) {
|
||||
return Center(
|
||||
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(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -1019,59 +1278,113 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
child: Text("基于库存查看记录统计", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6))),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Top 地点列表
|
||||
...data.topLocations.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final location = entry.value;
|
||||
final isTop3 = index < 3;
|
||||
|
||||
return FadeInUp(
|
||||
delay: Duration(milliseconds: 400 + index * 100),
|
||||
child: Container(
|
||||
width: 350,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
// 三排瀑布流横向滑动
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: SizedBox(
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
child: GestureDetector(
|
||||
onVerticalDragUpdate: (_) {}, // 拦截垂直拖拽
|
||||
child: Listener(
|
||||
onPointerSignal: (event) {
|
||||
if (event is PointerScrollEvent) {
|
||||
// 注册为唯一的滚轮事件处理者,阻止冒泡
|
||||
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(
|
||||
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),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
width: 22,
|
||||
height: 22,
|
||||
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,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"${index + 1}",
|
||||
"${actualIndex + 1}",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isTop3 ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 8),
|
||||
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(
|
||||
"${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) {
|
||||
final yearlyHours = data.yearlyPlayTime.inMinutes / 60;
|
||||
final kdRatio = data.yearlyDeathCount > 0
|
||||
? (data.yearlyKillCount / data.yearlyDeathCount).toStringAsFixed(2)
|
||||
: data.yearlyKillCount > 0
|
||||
? "∞"
|
||||
: "-";
|
||||
|
||||
// 构建数据项列表
|
||||
final dataItems = <_SummaryGridItem>[
|
||||
_SummaryGridItem("启动游戏", "${data.yearlyLaunchCount}", "次", FontAwesomeIcons.play, Colors.green),
|
||||
_SummaryGridItem("游玩时长", yearlyHours.toStringAsFixed(1), "小时", FontAwesomeIcons.clock, Colors.blue),
|
||||
_SummaryGridItem("游戏崩溃", "${data.yearlyCrashCount}", "次", FontAwesomeIcons.bug, Colors.orange),
|
||||
_SummaryGridItem("击杀玩家", "${data.yearlyKillCount}", "次", FontAwesomeIcons.crosshairs, Colors.green),
|
||||
_SummaryGridItem("意外死亡", "${data.yearlyDeathCount}", "次", FontAwesomeIcons.skull, Colors.red),
|
||||
_SummaryGridItem("KD 比率", kdRatio, "", FontAwesomeIcons.chartLine, Colors.teal),
|
||||
_SummaryGridItem("载具损毁", "${data.yearlyVehicleDestructionCount}", "次", FontAwesomeIcons.explosion, Colors.red),
|
||||
_SummaryGridItem("启动游戏", "${data.yearlyLaunchCount}", "次", FontAwesomeIcons.play, Colors.green, isWide: false),
|
||||
_SummaryGridItem(
|
||||
"游玩时长",
|
||||
yearlyHours.toStringAsFixed(1),
|
||||
"小时",
|
||||
FontAwesomeIcons.clock,
|
||||
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)
|
||||
_SummaryGridItem(
|
||||
"最长在线",
|
||||
@ -1130,17 +1458,9 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
"小时",
|
||||
FontAwesomeIcons.hourglassHalf,
|
||||
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)
|
||||
_SummaryGridItem(
|
||||
"最早时刻",
|
||||
@ -1148,6 +1468,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
"",
|
||||
FontAwesomeIcons.sun,
|
||||
Colors.orange,
|
||||
isWide: false,
|
||||
),
|
||||
if (data.latestPlayDate != null)
|
||||
_SummaryGridItem(
|
||||
@ -1156,8 +1477,57 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
"",
|
||||
FontAwesomeIcons.moon,
|
||||
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(
|
||||
@ -1173,7 +1543,7 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
Icon(FontAwesomeIcons.star, size: 20, color: Colors.yellow),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"2025 年度报告",
|
||||
"星际公民 2025 年度报告",
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@ -1195,19 +1565,21 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
// 数据网格
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: MasonryGridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
crossAxisCount: 5,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
itemCount: dataItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = dataItems[index];
|
||||
final isSmallFont = item.isWide; // 载具和位置使用较小字体
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
height: 150,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@ -1216,8 +1588,8 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(item.icon, size: 14, color: item.color.withValues(alpha: .8)),
|
||||
const SizedBox(height: 8),
|
||||
Icon(item.icon, size: 20, color: item.color.withValues(alpha: .8)),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
@ -1226,30 +1598,32 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
child: Text(
|
||||
item.value,
|
||||
style: TextStyle(
|
||||
fontSize: 34,
|
||||
fontSize: isSmallFont ? 16 : 42,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
height: 1.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: isSmallFont ? 2 : null,
|
||||
overflow: isSmallFont ? TextOverflow.ellipsis : null,
|
||||
),
|
||||
),
|
||||
if (item.unit.isNotEmpty) ...[
|
||||
const SizedBox(width: 2),
|
||||
const SizedBox(width: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
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(
|
||||
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,
|
||||
),
|
||||
],
|
||||
@ -1259,37 +1633,13 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 最爱载具
|
||||
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),
|
||||
const SizedBox(height: 20),
|
||||
// 底部标识
|
||||
FadeInUp(
|
||||
delay: const Duration(milliseconds: 600),
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: Text(
|
||||
"SC汉化盒子 · 2025年度报告",
|
||||
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .3)),
|
||||
"由 SC 汉化盒子为您呈现",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .3)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1302,11 +1652,11 @@ class YearlyReportUI extends HookConsumerWidget {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
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(
|
||||
"数据使用您的本地日志生成,不会发送到任何第三方。因跨版本 Log 改动较大,数据可能不完整,仅供娱乐。",
|
||||
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 IconData icon;
|
||||
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