// stats_page.dart — 统计页(月/周/日 周期卡 + 设备下拉 + 最近两周流量折线)。 // 单一数据源:近30天每日用量(usageProvider,autoDispose 进页重取),月/周/日 + 折线全从此聚合。 import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/usage_point.dart'; import '../pangolin_theme.dart'; import '../state/account_providers.dart'; import '../state/app_providers.dart'; import '../widgets/app_top_bar.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/period_card.dart'; import '../widgets/usage_line_chart.dart'; class StatsPage extends ConsumerWidget { const StatsPage({super.key, required this.isWide}); final bool isWide; @override Widget build(BuildContext context, WidgetRef ref) { final c = context.pangolin; final t = ref.watch(appTextProvider); final usage = ref.watch(usageProvider(30)).valueOrNull ?? const []; // 周期聚合:取最后 N 天的点求和。最后一点即今日(避开 UTC/本地日期换算)。 (int down, int up, int min) sumLast(int days) { var d = 0, u = 0, m = 0; final start = usage.length > days ? usage.length - days : 0; for (var i = start; i < usage.length; i++) { d += usage[i].bytesDown; u += usage[i].bytesUp; m += usage[i].minutesUsed; } return (d, u, m); } List metricsFor(int days) { final (d, u, m) = sumLast(days); final dn = _fmtBytes(d), up = _fmtBytes(u); return [ StatMetric(dn.$1, dn.$2, '↓ ${t.statDown}'), StatMetric(up.$1, up.$2, '↑ ${t.statUp}'), StatMetric((m / 60.0).toStringAsFixed(1), 'h', t.statDuration), ]; } // 折线:最后 14 天每日总流量(GB)。 final chart = [ for (var i = usage.length > 14 ? usage.length - 14 : 0; i < usage.length; i++) usage[i].gbTotal, ]; final body = RefreshIndicator( color: c.accent, backgroundColor: c.surface, onRefresh: () async { ref.invalidate(usageProvider(30)); ref.invalidate(deviceUsageProvider(30)); await ref.read(meProvider.notifier).refresh(); }, child: ListView( padding: EdgeInsets.fromLTRB(isWide ? 32 : 20, isWide ? 8 : 4, isWide ? 32 : 20, 24), children: [ // 顶行:标题(宽屏) + 设备下拉(右上角) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (isWide) Text(t.statsTitle, style: PangolinText.display.copyWith(color: c.fg1, fontSize: 20, fontWeight: FontWeight.w700)) else const SizedBox.shrink(), _DeviceDropdown(label: t.allDevices), ]), const SizedBox(height: 14), PeriodCard(period: t.periodMonth, metrics: metricsFor(30)), const SizedBox(height: 12), PeriodCard(period: t.periodWeek, metrics: metricsFor(7)), const SizedBox(height: 12), PeriodCard(period: t.periodToday, metrics: metricsFor(1)), const SizedBox(height: 20), // 折线卡 Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), decoration: BoxDecoration( color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm, ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(t.chartTwoWeeks, style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600, fontSize: 13)), Text('GB', style: PangolinText.mono.copyWith(color: c.fg3, fontSize: 11)), ]), const SizedBox(height: 10), UsageLineChart(values: chart, accent: c.accent, grid: c.border), ]), ), ], ), ); if (isWide) return body; return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ AppTopBar(brand: t.brand), PageTitle(title: t.statsTitle), Expanded(child: body), ]); } } /// 字节 → (数值, 单位):<1GB 显示 MB,否则 GB(各 1 位小数)。 (String, String) _fmtBytes(int bytes) { const gb = 1024 * 1024 * 1024, mb = 1024 * 1024; if (bytes >= gb) return ((bytes / gb).toStringAsFixed(1), 'GB'); return ((bytes / mb).toStringAsFixed(1), 'MB'); } /// 设备下拉(右上角):全部 + 近30天用过的设备名。 /// 当前每设备归因未打通(usage_device_daily 常空),先只挂「全部」; /// 设备注册打通后(TODO #9/#10)填充设备名 + 按设备过滤。 class _DeviceDropdown extends StatelessWidget { const _DeviceDropdown({required this.label}); final String label; @override Widget build(BuildContext context) { final c = context.pangolin; return PopupMenuButton( tooltip: '', offset: const Offset(0, 38), color: c.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(PangolinRadius.md), side: BorderSide(color: c.border), ), itemBuilder: (_) => [ PopupMenuItem(value: '', child: Text(label, style: PangolinText.sm.copyWith(color: c.fg1))), ], child: Container( padding: const EdgeInsets.fromLTRB(12, 6, 10, 6), decoration: BoxDecoration( color: c.surface, border: Border.all(color: c.borderStrong), borderRadius: BorderRadius.circular(PangolinRadius.full), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(PangolinIcons.smartphone, size: 14, color: c.accent), const SizedBox(width: 7), Text(label, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600, fontSize: 12.5)), const SizedBox(width: 5), Icon(PangolinIcons.chevronDown, size: 13, color: c.fg3), ]), ), ); } }