fix(client): 连接后延迟用内核 urltest 实测 + 侧栏套餐到期日接真
- 延迟:TCP 探针在隧道开启时会被路由进隧道而测不到(显示 —)。改为连接后 (phase==on)取内核 statsStream 的 urltest delayMs 最小正值(代理真实 RTT), 未连接时仍用节点 TCP 探针。连接页 _SpeedRow/_NodePill/_CurrentNodeCard 与统计页平均延迟统一此口径。 - plan_badge_card(侧栏)接 me.expiresAt 显示真实到期日(原只显示「有效期至」 无日期);account_page 之外漏改的最后一处。 flutter analyze 0 error;114 tests passed。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../bridge/vpn_bridge.dart';
|
||||
import '../core/responsive/form_factor.dart';
|
||||
import '../l10n/app_text.dart';
|
||||
import '../models/node.dart';
|
||||
@@ -34,6 +35,11 @@ class ConnectPage extends ConsumerWidget {
|
||||
final stats = ref.watch(vpnStatsProvider).valueOrNull;
|
||||
final down = _mbps(stats?.downloadSpeed);
|
||||
final up = _mbps(stats?.uploadSpeed);
|
||||
// 延迟来源:连接后用内核 urltest 实测(代理真实 RTT,TCP 探针被隧道吞掉时仍准);
|
||||
// 未连接时用节点 TCP 探针 ping。0/无 → 显示 —。
|
||||
final livePing = conn.phase == VpnPhase.on ? _bestUrltest(stats) : node.ping;
|
||||
final pingLabel = livePing > 0 ? '${livePing}ms' : '—';
|
||||
final latencyValue = livePing > 0 ? '$livePing' : '—';
|
||||
|
||||
final caption = switch (conn.phase) {
|
||||
VpnPhase.off => t.capOff,
|
||||
@@ -65,8 +71,8 @@ class ConnectPage extends ConsumerWidget {
|
||||
final infoChildren = <Widget>[
|
||||
if (isFree)
|
||||
QuotaCard(quota: quota, t: t, onWatchAd: () => ref.read(quotaProvider.notifier).watchAd()),
|
||||
if (conn.phase == VpnPhase.on) _SpeedRow(t: t, node: node, down: down, up: up),
|
||||
_CurrentNodeCard(t: t, node: node, smart: smart, showLabel: isWide, onTap: onOpenNodes),
|
||||
if (conn.phase == VpnPhase.on) _SpeedRow(t: t, down: down, up: up, latency: latencyValue),
|
||||
_CurrentNodeCard(t: t, node: node, smart: smart, pingLabel: pingLabel, showLabel: isWide, onTap: onOpenNodes),
|
||||
];
|
||||
|
||||
final statusTrailing = Text(
|
||||
@@ -104,7 +110,7 @@ class ConnectPage extends ConsumerWidget {
|
||||
style: PangolinText.caption.copyWith(color: c.danger, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
],
|
||||
_NodePill(t: t, node: node, smart: smart),
|
||||
_NodePill(t: t, node: node, smart: smart, pingLabel: pingLabel),
|
||||
if (isFree) ...[
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
@@ -114,7 +120,7 @@ class ConnectPage extends ConsumerWidget {
|
||||
],
|
||||
if (conn.phase == VpnPhase.on) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SpeedRow(t: t, node: node, down: down, up: up),
|
||||
_SpeedRow(t: t, down: down, up: up, latency: latencyValue),
|
||||
],
|
||||
]),
|
||||
),
|
||||
@@ -199,13 +205,20 @@ class ConnectPage extends ConsumerWidget {
|
||||
String _mbps(double? bytesPerSec) =>
|
||||
bytesPerSec == null ? '—' : (bytesPerSec * 8 / 1000000).toStringAsFixed(1);
|
||||
|
||||
/// 内核 urltest 各出站中的最小正延迟(ms);无则 0。
|
||||
int _bestUrltest(VpnStatsEvent? s) {
|
||||
if (s == null) return 0;
|
||||
final ds = s.urltestResults.where((r) => r.delayMs > 0).map((r) => r.delayMs);
|
||||
return ds.isEmpty ? 0 : ds.reduce((a, b) => a < b ? a : b);
|
||||
}
|
||||
|
||||
/// 实时速率行(连接成功时显示;来自内核 statsStream 实时数据)。
|
||||
class _SpeedRow extends StatelessWidget {
|
||||
const _SpeedRow({required this.t, required this.node, required this.down, required this.up});
|
||||
const _SpeedRow({required this.t, required this.down, required this.up, required this.latency});
|
||||
final AppText t;
|
||||
final Node node;
|
||||
final String down;
|
||||
final String up;
|
||||
final String latency;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -213,7 +226,7 @@ class _SpeedRow extends StatelessWidget {
|
||||
final metrics = [
|
||||
(PangolinIcons.arrowDown, t.download, down, 'Mb/s'),
|
||||
(PangolinIcons.arrowUp, t.upload, up, 'Mb/s'),
|
||||
(PangolinIcons.zap, t.latency, node.ping > 0 ? '${node.ping}' : '—', 'ms'),
|
||||
(PangolinIcons.zap, t.latency, latency, 'ms'),
|
||||
];
|
||||
return Row(children: [
|
||||
for (var i = 0; i < metrics.length; i++)
|
||||
@@ -258,12 +271,14 @@ class _CurrentNodeCard extends StatelessWidget {
|
||||
required this.t,
|
||||
required this.node,
|
||||
required this.smart,
|
||||
required this.pingLabel,
|
||||
required this.showLabel,
|
||||
required this.onTap,
|
||||
});
|
||||
final AppText t;
|
||||
final Node node;
|
||||
final bool smart;
|
||||
final String pingLabel;
|
||||
final bool showLabel;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@@ -272,8 +287,8 @@ class _CurrentNodeCard extends StatelessWidget {
|
||||
final c = context.pangolin;
|
||||
final title = smart ? t.smartSelect : node.localizedName(t.lang);
|
||||
final sub = smart
|
||||
? '${node.localizedName(t.lang)} · ${node.pingLabel}'
|
||||
: '${node.localizedSub(t.lang)} · ${node.pingLabel}';
|
||||
? '${node.localizedName(t.lang)} · $pingLabel'
|
||||
: '${node.localizedSub(t.lang)} · $pingLabel';
|
||||
return Material(
|
||||
color: c.surface,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -314,17 +329,18 @@ class _CurrentNodeCard extends StatelessWidget {
|
||||
|
||||
/// 桌面连接页节点胶囊([zap] 智能选择 · 节点名 · 延迟ms)。
|
||||
class _NodePill extends StatelessWidget {
|
||||
const _NodePill({required this.t, required this.node, required this.smart});
|
||||
const _NodePill({required this.t, required this.node, required this.smart, required this.pingLabel});
|
||||
final AppText t;
|
||||
final Node node;
|
||||
final bool smart;
|
||||
final String pingLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = context.pangolin;
|
||||
final label = smart
|
||||
? '${t.smartSelect} · ${node.localizedName(t.lang)} · ${node.pingLabel}'
|
||||
: '${node.localizedName(t.lang)} · ${node.pingLabel}';
|
||||
? '${t.smartSelect} · ${node.localizedName(t.lang)} · $pingLabel'
|
||||
: '${node.localizedName(t.lang)} · $pingLabel';
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
Reference in New Issue
Block a user