feat(client/nodes): 节点页刷新 — 下拉刷新 + 刷新按钮 + 进页刷新 + 10s 限流

- nodesProvider.refresh:10s 限流(下拉/按钮/进页多次触发只打一次远端);
  不清空当前列表(后台拉、好了再换,失败保留旧数据),避免进页/下拉闪空
- nodes_page:initState 进页自动刷一次;RefreshIndicator 下拉刷新(移动端);
  搜索框右侧 _RefreshButton 点击刷新(刷新中 spinner)
servers 页 golden 重生成(搜索行加按钮);CI 闸 golden 未动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-28 22:00:26 +08:00
parent 51c171d815
commit 0542ff027f
6 changed files with 91 additions and 18 deletions
+14 -2
View File
@@ -17,6 +17,10 @@ import 'auth_provider.dart';
class NodesNotifier extends AsyncNotifier<List<Node>> {
bool _disposed = false;
DateTime? _lastFetch;
/// 刷新限流:距上次成功拉取不足此间隔则跳过远端(下拉/按钮/进页多次触发只打一次)。
static const _minInterval = Duration(seconds: 10);
@override
Future<List<Node>> build() async {
@@ -24,22 +28,30 @@ class NodesNotifier extends AsyncNotifier<List<Node>> {
final auth = ref.watch(authProvider);
if (!auth.isLoggedIn) return const [];
final list = await _fetchNodes(auth.accessToken!);
_lastFetch = DateTime.now();
unawaited(_measure(list));
return list;
}
/// 刷新节点列表(下拉/按钮/进页调用)。10s 限流;不清空当前列表(后台拉、好了再换),
/// 避免进页/下拉时列表闪空;失败保留旧数据。
Future<void> refresh() async {
final auth = ref.read(authProvider);
if (!auth.isLoggedIn) {
state = const AsyncData([]);
return;
}
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
if (_lastFetch != null && DateTime.now().difference(_lastFetch!) < _minInterval) {
return; // 限流:10s 内不重复请求远端
}
final result = await AsyncValue.guard(() async {
final list = await _fetchNodes(auth.accessToken!);
_lastFetch = DateTime.now();
unawaited(_measure(list));
return list;
});
if (_disposed) return;
if (result.hasValue) state = result; // 成功才换;失败保留旧列表
}
/// 后台实测各节点延迟,完成后回填(notifier 未销毁才更新)。