feat(stats): 统计页设备下拉接真实设备(#10 第①层)
ci-pangolin / Lint — shellcheck (push) Successful in 5s
ci-pangolin / OpenAPI Sync Check (push) Successful in 21s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Successful in 24s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 4s
ci-pangolin / Go — build + test (push) Successful in 9s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m6s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s

_DeviceDropdown 改 ConsumerWidget:菜单 = 「全部」+ 近 30 天有用量的真实设备名
(来自 deviceUsageProvider);选中态存 statsDeviceProvider。无每设备用量时只显示
「全部」(稀疏,正常)。第②层「按设备过滤月/周/日卡+折线」待 per-device 每日曲线
API(#10 第②层),本提交只接下拉内容。tablet 统计 golden 无变化(空设备时同旧观感)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 07:04:20 +08:00
parent f2499a64e1
commit 5dcebb77fd
+29 -9
View File
@@ -3,6 +3,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_text.dart';
import '../models/device_usage.dart';
import '../models/usage_point.dart';
import '../pangolin_theme.dart';
import '../state/account_providers.dart';
@@ -74,7 +76,7 @@ class StatsPage extends ConsumerWidget {
style: PangolinText.display.copyWith(color: c.fg1, fontSize: 20, fontWeight: FontWeight.w700))
else
const SizedBox.shrink(),
_DeviceDropdown(label: t.allDevices),
_DeviceDropdown(t: t),
]),
const SizedBox(height: 14),
PeriodCard(period: t.periodMonth, metrics: metricsFor(30)),
@@ -129,16 +131,28 @@ class StatsPage extends ConsumerWidget {
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;
/// 统计页选中的设备(null = 全部;否则 device uuid)。第②层(按设备过滤卡/图)
/// 待 per-device 每日曲线 API(#10);本层先把下拉内容接成真实设备。
final statsDeviceProvider = StateProvider<String?>((ref) => null);
/// 设备下拉(右上角):「全部」+ 近 30 天有用量的真实设备名(来自 deviceUsageProvider)。
/// 无每设备用量时只显示「全部」(稀疏,正常)。
class _DeviceDropdown extends ConsumerWidget {
const _DeviceDropdown({required this.t});
final AppText t;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final c = context.pangolin;
final devices = ref.watch(deviceUsageProvider(30)).valueOrNull ?? const <DeviceUsage>[];
final selected = ref.watch(statsDeviceProvider);
String nameOf(String uuid) => devices
.firstWhere((d) => d.uuid == uuid, orElse: () => const DeviceUsage(uuid: '', name: '', platform: '', bytesUp: 0, bytesDown: 0, minutesUsed: 0))
.name;
final label = selected == null
? t.allDevices
: (nameOf(selected).isNotEmpty ? nameOf(selected) : t.allDevices);
return PopupMenuButton<String>(
tooltip: '',
offset: const Offset(0, 38),
@@ -147,8 +161,14 @@ class _DeviceDropdown extends StatelessWidget {
borderRadius: BorderRadius.circular(PangolinRadius.md),
side: BorderSide(color: c.border),
),
onSelected: (v) => ref.read(statsDeviceProvider.notifier).state = v.isEmpty ? null : v,
itemBuilder: (_) => [
PopupMenuItem(value: '', child: Text(label, style: PangolinText.sm.copyWith(color: c.fg1))),
PopupMenuItem(value: '', child: Text(t.allDevices, style: PangolinText.sm.copyWith(color: c.fg1))),
for (final d in devices)
PopupMenuItem(
value: d.uuid,
child: Text(d.name.isNotEmpty ? d.name : d.platform, style: PangolinText.sm.copyWith(color: c.fg1)),
),
],
child: Container(
padding: const EdgeInsets.fromLTRB(12, 6, 10, 6),