diff --git a/client/lib/screens/settings_page.dart b/client/lib/screens/settings_page.dart index 28bd1d0..5071687 100644 --- a/client/lib/screens/settings_page.dart +++ b/client/lib/screens/settings_page.dart @@ -1,25 +1,21 @@ // settings_page.dart — 设置(desktop 一级页) // -// 对照 ui_kits/desktop/dapp.jsx DSettings(精简):兑换入口 + 语言段控 + -// 深色外观 + 协议 + 版本。开关组(开机自启/智能分流/Kill-switch)待 l10n 补齐后加。 +// 三组:连接(开机自启/自动连接/智能分流/kill-switch 开关)· 外观(语言/深色)· +// 关于(协议/检查更新/版本 + Web 用户中心链接)。购买/订单入口已归 Account 页。 import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../core/responsive/form_factor.dart'; import '../l10n/app_text.dart'; import '../pangolin_theme.dart'; import '../services/web_launch.dart'; import '../state/app_providers.dart'; -import '../state/navigation_provider.dart'; import '../state/settings_provider.dart'; import '../state/update_provider.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/pangolin_toast.dart'; -import 'orders_page.dart'; -import 'payment_page.dart'; -import 'purchase_page.dart'; + /// 「检查更新」手动触发:拉取 `$kApiBaseUrl/version`,失败/无更新走轻提示, /// 有更新则清掉「已忽略版本」→ 顶部更新 banner 显示 + toast(不弹窗)。 Future _checkForUpdate(BuildContext context, WidgetRef ref, AppText t) async { @@ -56,39 +52,13 @@ class SettingsPage extends ConsumerWidget { Widget sw(bool v, ValueChanged on) => Switch(value: v, activeThumbColor: c.accent, onChanged: on); - // desktop 内容区下钻(切 navView);mobile/tablet 全屏 push(与 account_page 同)。 - final isDesktop = context.formFactor == FormFactor.desktop; - void open(NavView v, Widget mobilePage) { - if (isDesktop) { - ref.read(navViewProvider.notifier).state = v; - } else { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => mobilePage)); - } - } - return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(32, 16, 32, 24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // 购买套餐 + 我的订单 - _Card(children: [ - _Row(title: t.purchaseTitle, sub: t.proMember, - right: Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3), - onTap: () => open( - NavView.purchase, - PurchaseScreen( - t: t, - onOrderCreated: () => Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => PaymentScreen(t: t))), - ), - )), - _Row(title: t.ordersTitle, last: true, - right: Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3), - onTap: () => open(NavView.orders, OrdersScreen(t: t))), - ]), - const SizedBox(height: 16), - // 功能开关组(开机自启是桌面端登录项概念,iOS/Android 无此能力,仅桌面显示) + // ① 连接(功能开关组;开机自启是桌面端登录项概念,iOS/Android 无此能力,仅桌面显示) + _SectionLabel(text: t.settingsGroupConn), _Card(children: [ if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) _Row(title: t.autostart, sub: t.autostartSub, right: sw(s.autostart, settings.setAutostart)), @@ -96,19 +66,26 @@ class SettingsPage extends ConsumerWidget { _Row(title: t.smartRoute, sub: t.smartRouteSub, right: sw(s.smartRoute, settings.setSmartRoute)), _Row(title: t.killSwitch, sub: t.killSwitchSub, last: true, right: sw(s.killSwitch, settings.setKillSwitch)), ]), - const SizedBox(height: 16), - // 配置组 + const SizedBox(height: 20), + // ② 外观(语言 + 深色) + _SectionLabel(text: t.settingsGroupAppearance), _Card(children: [ _Row(title: t.language, right: _LangSwitch(lang: lang, onPick: (l) => ref.read(localeProvider.notifier).set(l))), _Row( title: t.darkAppearance, sub: isDark ? t.stateOn : t.followLight, + last: true, right: sw(isDark, (v) => ref.read(themeModeProvider.notifier).state = v ? ThemeMode.dark : ThemeMode.light), ), + ]), + const SizedBox(height: 20), + // ③ 关于(协议 + 检查更新 + 版本 + 底部 Web 用户中心链接) + _SectionLabel(text: t.settingsGroupAbout), + _Card(children: [ _Row(title: t.protocol, right: Text('REALITY / Hysteria2', style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), - _Row(title: t.webUserCenter, right: Icon(PangolinIcons.externalLink, size: 18, color: c.fg3), onTap: () => openWebUserCenter(ref)), _Row(title: t.checkUpdate, right: Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3), onTap: () => _checkForUpdate(context, ref, t)), - _Row(title: 'Version', last: true, right: Text(version, style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), + _Row(title: 'Version', right: Text(version, style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), + _Row(title: t.webUserCenter, last: true, right: Icon(PangolinIcons.externalLink, size: 18, color: c.fg3), onTap: () => openWebUserCenter(ref)), ]), ]), ), @@ -116,6 +93,19 @@ class SettingsPage extends ConsumerWidget { } } +class _SectionLabel extends StatelessWidget { + const _SectionLabel({required this.text}); + final String text; + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 10), + child: Text(text, style: PangolinText.sm.copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 13)), + ); + } +} + class _Card extends StatelessWidget { const _Card({required this.children}); final List children; diff --git a/client/test/golden/goldens/desktop_settings.png b/client/test/golden/goldens/desktop_settings.png index 58debed..44d3e17 100644 Binary files a/client/test/golden/goldens/desktop_settings.png and b/client/test/golden/goldens/desktop_settings.png differ diff --git a/client/test/widget/settings_ia_test.dart b/client/test/widget/settings_ia_test.dart new file mode 100644 index 0000000..0938f28 --- /dev/null +++ b/client/test/widget/settings_ia_test.dart @@ -0,0 +1,85 @@ +// settings_ia_test.dart — 设置页 IA 结构断言:三个分组标题存在 / 购买入口不在设置页(归 Account)。 +// +// 桌面态(windows 平台 + 宽 surface),经 HomeShell 渲染真实 DesktopShell → SettingsPage, +// 与 account_ia_test.dart 同一套 harness。 +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pangolin_vpn/bridge/vpn_bridge_mock.dart'; +import 'package:pangolin_vpn/bridge/vpn_bridge_provider.dart'; +import 'package:pangolin_vpn/l10n/app_text.dart'; +import 'package:pangolin_vpn/l10n/strings_zh.dart'; +import 'package:pangolin_vpn/pangolin_theme.dart'; +import 'package:pangolin_vpn/screens/settings_page.dart'; +import 'package:pangolin_vpn/services/token_store.dart'; +import 'package:pangolin_vpn/shell/desktop_shell.dart'; +import 'package:pangolin_vpn/shell/home_shell.dart'; +import 'package:pangolin_vpn/state/app_providers.dart'; +import 'package:pangolin_vpn/state/auth_provider.dart'; +import 'package:pangolin_vpn/state/navigation_provider.dart'; + +import '../helpers/harness.dart'; + +final AppText t = StringsZh(); + +class _NullTokenStore implements TokenStore { + const _NullTokenStore(); + @override + Future saveTokens({required String access, required String refresh}) async {} + @override + Future loadAccessToken() async => null; + @override + Future loadRefreshToken() async => null; + @override + Future clear() async {} + @override + Future markOnboarded() async {} + @override + Future isOnboarded() async => true; + @override + Future saveLastEmail(String email) async {} + @override + Future loadLastEmail() async => null; +} + +void main() { + setUpAll(disableGoogleFontsFetching); + + testWidgets('settings page IA: three group labels present / purchase entry moved out to account', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.windows; + tester.view.physicalSize = const Size(920, 1400); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + try { + await tester.pumpWidget(ProviderScope( + overrides: [ + tokenStoreProvider.overrideWithValue(const _NullTokenStore()), + vpnBridgeProvider.overrideWithValue(VpnBridgeMock()), + appTextProvider.overrideWithValue(t), + navViewProvider.overrideWith((ref) => NavView.settings), + ], + child: MaterialApp( + debugShowCheckedModeBanner: false, + theme: PangolinTheme.light, + home: const HomeShell(), + ), + )); + await tester.pumpAndSettle(); + + expect(find.byType(DesktopShell), findsOneWidget); + expect(find.byType(SettingsPage), findsOneWidget); + + // 三个分组标题存在(连接/外观/关于)。 + expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.settingsGroupConn)), findsOneWidget); + expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.settingsGroupAppearance)), findsOneWidget); + expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.settingsGroupAbout)), findsOneWidget); + + // 购买入口不在设置页(归 Account)。 + expect(find.descendant(of: find.byType(SettingsPage), matching: find.text(t.purchaseTitle)), findsNothing); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); +}