diff --git a/client/lib/screens/nodes_page.dart b/client/lib/screens/nodes_page.dart index 8f5013c..5764d1b 100644 --- a/client/lib/screens/nodes_page.dart +++ b/client/lib/screens/nodes_page.dart @@ -50,17 +50,20 @@ class _NodesPageState extends ConsumerState { } // 点节点:未连接(off)直接选→跳连接页,无破坏性;已连接/连接中切换会断开重连, - // 弹确认防误触(误碰节点把当前连接切走)。 - void _onTapNode(Node n) { - final phase = ref.read(connectionProvider).phase; - if (phase == VpnPhase.off) { - _pick(n.code); + // 弹确认防误触(误碰把当前连接切走)。节点卡与智能选择卡共用此逻辑。 + void _onTapNode(Node n) => _onSelect(n.code, n.localizedName(ref.read(appTextProvider).lang)); + + void _onTapSmart() => _onSelect(kSmartNodeCode, ref.read(appTextProvider).smartSelect); + + void _onSelect(String code, String name) { + if (ref.read(connectionProvider).phase == VpnPhase.off) { + _pick(code); return; } - _confirmSwitch(n); + _confirmSwitch(code, name); } - Future _confirmSwitch(Node n) async { + Future _confirmSwitch(String code, String name) async { final c = context.pangolin; final t = ref.read(appTextProvider); final ok = await showDialog( @@ -68,7 +71,7 @@ class _NodesPageState extends ConsumerState { builder: (ctx) => AlertDialog( backgroundColor: c.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(PangolinRadius.xl)), - title: Text(t.nodeSwitchTitle(n.localizedName(t.lang)), + title: Text(t.nodeSwitchTitle(name), style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), content: Text(t.nodeSwitchBody, style: PangolinText.sm.copyWith(color: c.fg2, height: 1.5)), actions: [ @@ -83,7 +86,7 @@ class _NodesPageState extends ConsumerState { ], ), ); - if (ok == true) _pick(n.code); + if (ok == true) _pick(code); } List _filtered(List nodes) { @@ -102,7 +105,7 @@ class _NodesPageState extends ConsumerState { final nodes = _filtered(ref.watch(nodesProvider).valueOrNull ?? const []); final smart = selected == kSmartNodeCode; - final smartCard = SmartSelectCard(t: t, selected: smart, onTap: () => _pick(kSmartNodeCode)); + final smartCard = SmartSelectCard(t: t, selected: smart, onTap: _onTapSmart); // 搜索框 + 刷新按钮(点击刷新);列表本身支持下拉刷新。 final searchRow = Row(children: [ Expanded(child: _SearchBox(hint: t.searchPh, onChanged: (v) => setState(() => _q = v))), diff --git a/client/test/widget/node_connect_confirm_test.dart b/client/test/widget/node_connect_confirm_test.dart index 476094a..e1e1395 100644 --- a/client/test/widget/node_connect_confirm_test.dart +++ b/client/test/widget/node_connect_confirm_test.dart @@ -14,6 +14,7 @@ import 'package:pangolin_vpn/pangolin_theme.dart'; import 'package:pangolin_vpn/screens/nodes_page.dart'; import 'package:pangolin_vpn/state/connection_provider.dart'; import 'package:pangolin_vpn/state/nodes_provider.dart'; +import 'package:pangolin_vpn/widgets/smart_select_card.dart'; import '../helpers/harness.dart'; @@ -98,4 +99,34 @@ void main() { await bridge.stop(); await tester.pump(); }); + + testWidgets('已连接:点智能选择卡也弹确认 → 取消不切换', (tester) async { + final bridge = VpnBridgeMock(connectDelay: const Duration(milliseconds: 10)); + final container = makeContainer(bridge); + addTearDown(bridge.dispose); + addTearDown(container.dispose); + // 先选具体节点(否则默认就是智能选择),便于验证取消不切到智能。 + container.read(selectedNodeCodeProvider.notifier).state = 'HK'; + await pump(tester, container); + + container.read(connectionProvider); + await bridge.start('{}'); + await tester.pump(const Duration(milliseconds: 15)); + expect(container.read(connectionProvider).phase, VpnPhase.on); + + await tester.tap(find.byType(SmartSelectCard)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 350)); + + expect(find.text(t.nodeSwitchTitle(t.smartSelect)), findsOneWidget, reason: '智能选择也应弹确认'); + expect(container.read(selectedNodeCodeProvider), 'HK', reason: '弹窗期间未切到智能'); + + await tester.tap(find.text(t.devCancel)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 350)); + expect(container.read(selectedNodeCodeProvider), 'HK', reason: '取消后维持原节点'); + + await bridge.stop(); + await tester.pump(); + }); }