fix(client): 统计页/连接页 加载时整页转圈,先拉数据再渲染(不闪默认态)
ci-pangolin / Lint — shellcheck (push) Successful in 5s
ci-pangolin / OpenAPI Sync Check (push) Successful in 17s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 5s
ci-pangolin / Flutter — analyze + test (push) Successful in 22s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
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) Successful in 4m11s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s

之前 .valueOrNull ?? 默认 → 数据没到时先渲染默认态(统计 0 / 连接页免费广告),
me/usage 到了再跳真实态,闪烁体验差。改为关键 provider 首拉(loading 且无缓存)时
整页 CircularProgressIndicator:
- 统计页:gate on usageProvider(30)
- 连接页:gate on 新增 meLoadingProvider(me 是常驻 Notifier,只首启转一下)
未登录/golden 场景 me 同步返回默认=不加载,不影响 golden。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-28 20:17:39 +08:00
parent 07f09a1600
commit ea1af967a0
3 changed files with 19 additions and 2 deletions
+5
View File
@@ -34,6 +34,11 @@ class ConnectPage extends ConsumerWidget {
final isFree = ref.watch(isFreePlanProvider);
final quota = ref.watch(quotaProvider);
final stats = ref.watch(vpnStatsProvider).valueOrNull;
// 账户首拉中 → 整页转圈,先拉到 me(决定免费/会员)再渲染,
// 避免「先闪免费广告、me 到了再跳会员」。me 是常驻 Notifier,只首启时转一下。
if (ref.watch(meLoadingProvider)) {
return Center(child: CircularProgressIndicator(color: c.accent, strokeWidth: 2.6));
}
final down = formatSpeed(stats?.downloadSpeed);
final up = formatSpeed(stats?.uploadSpeed);
// 延迟来源:连接后优先用内核 urltest 实测(代理真实 RTT);urltest 还没探过时
+7 -2
View File
@@ -20,7 +20,10 @@ class StatsPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final c = context.pangolin;
final t = ref.watch(appTextProvider);
final usage = ref.watch(usageProvider(30)).valueOrNull ?? const <UsagePoint>[];
final usageAsync = ref.watch(usageProvider(30));
final usage = usageAsync.valueOrNull ?? const <UsagePoint>[];
// 加载中且无缓存 → 整页转圈(先拉数据再渲染,不闪默认 0)。
final loading = usageAsync.isLoading && !usageAsync.hasValue;
// 周期聚合:取最后 N 天的点求和。最后一点即今日(避开 UTC/本地日期换算)。
(int down, int up, int min) sumLast(int days) {
@@ -49,7 +52,9 @@ class StatsPage extends ConsumerWidget {
final chartVals = chartPts.map((p) => p.gbTotal).toList();
final chartDates = chartPts.map((p) => p.date).toList();
final body = RefreshIndicator(
final body = loading
? Center(child: CircularProgressIndicator(color: c.accent, strokeWidth: 2.6))
: RefreshIndicator(
color: c.accent,
backgroundColor: c.surface,
onRefresh: () async {
+7
View File
@@ -25,3 +25,10 @@ final isFreePlanProvider = Provider<bool>((ref) {
final me = ref.watch(meProvider).valueOrNull;
return me?.isFree ?? true;
});
/// 账户聚合是否仍在「首拉」(加载中且无缓存)。连接页/统计页据此先转圈再渲染,
/// 避免免费默认态闪一下(广告/0)再跳成真实数据。
final meLoadingProvider = Provider<bool>((ref) {
final me = ref.watch(meProvider);
return me.isLoading && !me.hasValue;
});