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
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:
@@ -3,6 +3,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../l10n/app_text.dart';
|
||||||
|
import '../models/device_usage.dart';
|
||||||
import '../models/usage_point.dart';
|
import '../models/usage_point.dart';
|
||||||
import '../pangolin_theme.dart';
|
import '../pangolin_theme.dart';
|
||||||
import '../state/account_providers.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))
|
style: PangolinText.display.copyWith(color: c.fg1, fontSize: 20, fontWeight: FontWeight.w700))
|
||||||
else
|
else
|
||||||
const SizedBox.shrink(),
|
const SizedBox.shrink(),
|
||||||
_DeviceDropdown(label: t.allDevices),
|
_DeviceDropdown(t: t),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
PeriodCard(period: t.periodMonth, metrics: metricsFor(30)),
|
PeriodCard(period: t.periodMonth, metrics: metricsFor(30)),
|
||||||
@@ -129,16 +131,28 @@ class StatsPage extends ConsumerWidget {
|
|||||||
return ((bytes / mb).toStringAsFixed(1), 'MB');
|
return ((bytes / mb).toStringAsFixed(1), 'MB');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设备下拉(右上角):全部 + 近30天用过的设备名。
|
/// 统计页选中的设备(null = 全部;否则 device uuid)。第②层(按设备过滤卡/图)
|
||||||
/// 当前每设备归因未打通(usage_device_daily 常空),先只挂「全部」;
|
/// 待 per-device 每日曲线 API(#10);本层先把下拉内容接成真实设备。
|
||||||
/// 设备注册打通后(TODO #9/#10)填充设备名 + 按设备过滤。
|
final statsDeviceProvider = StateProvider<String?>((ref) => null);
|
||||||
class _DeviceDropdown extends StatelessWidget {
|
|
||||||
const _DeviceDropdown({required this.label});
|
/// 设备下拉(右上角):「全部」+ 近 30 天有用量的真实设备名(来自 deviceUsageProvider)。
|
||||||
final String label;
|
/// 无每设备用量时只显示「全部」(稀疏,正常)。
|
||||||
|
class _DeviceDropdown extends ConsumerWidget {
|
||||||
|
const _DeviceDropdown({required this.t});
|
||||||
|
final AppText t;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final c = context.pangolin;
|
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>(
|
return PopupMenuButton<String>(
|
||||||
tooltip: '',
|
tooltip: '',
|
||||||
offset: const Offset(0, 38),
|
offset: const Offset(0, 38),
|
||||||
@@ -147,8 +161,14 @@ class _DeviceDropdown extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(PangolinRadius.md),
|
borderRadius: BorderRadius.circular(PangolinRadius.md),
|
||||||
side: BorderSide(color: c.border),
|
side: BorderSide(color: c.border),
|
||||||
),
|
),
|
||||||
|
onSelected: (v) => ref.read(statsDeviceProvider.notifier).state = v.isEmpty ? null : v,
|
||||||
itemBuilder: (_) => [
|
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(
|
child: Container(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 6, 10, 6),
|
padding: const EdgeInsets.fromLTRB(12, 6, 10, 6),
|
||||||
|
|||||||
Reference in New Issue
Block a user