feat(client): 智能选择卡也走切换确认(防误触)

把节点卡的"已连接时切换先弹确认"逻辑抽成 _onSelect(code,name),节点卡与智能
选择卡共用。点智能选择卡:未连接直接选,已连接/连接中弹确认("切换到「智能选择」?")。
node_connect_confirm_test 增"已连接点智能选择卡弹确认→取消不切"用例。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 11:52:11 +08:00
parent 0820397a14
commit ddf8b1d4d8
2 changed files with 44 additions and 10 deletions
+13 -10
View File
@@ -50,17 +50,20 @@ class _NodesPageState extends ConsumerState<NodesPage> {
}
// 点节点:未连接(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<void> _confirmSwitch(Node n) async {
Future<void> _confirmSwitch(String code, String name) async {
final c = context.pangolin;
final t = ref.read(appTextProvider);
final ok = await showDialog<bool>(
@@ -68,7 +71,7 @@ class _NodesPageState extends ConsumerState<NodesPage> {
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<NodesPage> {
],
),
);
if (ok == true) _pick(n.code);
if (ok == true) _pick(code);
}
List<Node> _filtered(List<Node> nodes) {
@@ -102,7 +105,7 @@ class _NodesPageState extends ConsumerState<NodesPage> {
final nodes = _filtered(ref.watch(nodesProvider).valueOrNull ?? const <Node>[]);
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))),
@@ -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();
});
}