diff --git a/client/lib/state/account_providers.dart b/client/lib/state/account_providers.dart index 1aea2af..8cf5fe0 100644 --- a/client/lib/state/account_providers.dart +++ b/client/lib/state/account_providers.dart @@ -89,6 +89,18 @@ class DevicesNotifier extends AsyncNotifier> { await future; } + /// 静默刷新(不闪 loading):重新拉列表、成功才替换。供设备页 ~15s 轮询,让其他设备的 + /// 在线/状态变化 ~15s 内可见(配合服务端会话轮询刷 last_seen)。 + Future refresh() async { + if (!ref.read(authProvider).isLoggedIn) return; + try { + final list = await ref.read(accountApiProvider).devices(); + state = AsyncData(list); + } catch (_) { + // 失败保留旧列表,不闪错。 + } + } + /// 强制退出(吊销该设备会话,设备保留)后刷新列表。 Future forceLogout(String uuid) async { await ref.read(accountApiProvider).forceLogout(uuid); diff --git a/client/lib/widgets/account_screens.dart b/client/lib/widgets/account_screens.dart index 9ac2a0e..c5b3903 100644 --- a/client/lib/widgets/account_screens.dart +++ b/client/lib/widgets/account_screens.dart @@ -2,6 +2,8 @@ // // 文案全部经 AppText(l10n,单显)。套餐数字以 design/CLAUDE.md §7 为准。 // App 内无任何支付表单——购买仅引导至外部渠道。 +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -162,7 +164,8 @@ class DevicesScreen extends ConsumerWidget { ]), ); - return _SubScaffold(title: t.myDevices, onBack: onBack, embedded: embedded, child: content()); + return _SubScaffold( + title: t.myDevices, onBack: onBack, embedded: embedded, child: _DevicesAutoRefresh(child: content())); } Widget _row(BuildContext context, WidgetRef ref, PangolinScheme c, Device d, bool isLocal, bool divider) { @@ -493,3 +496,32 @@ class ContactScreen extends StatelessWidget { ); } } + +/// 透传式轮询包装器:在设备页期间每 15s 静默刷新设备列表(不闪 loading), +/// 让其他设备的在线/状态变化 ~15s 内可见。原样返回 child,不改布局。 +class _DevicesAutoRefresh extends ConsumerStatefulWidget { + const _DevicesAutoRefresh({required this.child}); + final Widget child; + @override + ConsumerState<_DevicesAutoRefresh> createState() => _DevicesAutoRefreshState(); +} + +class _DevicesAutoRefreshState extends ConsumerState<_DevicesAutoRefresh> { + Timer? _poll; + @override + void initState() { + super.initState(); + _poll = Timer.periodic(const Duration(seconds: 15), (_) { + if (mounted) ref.read(devicesProvider.notifier).refresh(); + }); + } + + @override + void dispose() { + _poll?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +}