feat(settings): 删购买/订单卡 + 归连接/外观/关于三组 + Web 链接下沉底部
购买套餐/我的订单入口已归 Account 页,设置页不再重复;设置页重组为 「连接」(开关组)/「外观」(语言+深色)/「关于」(协议/检查更新/版本+ Web 用户中心链接下沉底部)三组,标题走 l10n settingsGroupConn/Appearance/About。 新增 settings_ia_test.dart(TDD)+ 重录 desktop_settings golden。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,21 @@
|
|||||||
// settings_page.dart — 设置(desktop 一级页)
|
// settings_page.dart — 设置(desktop 一级页)
|
||||||
//
|
//
|
||||||
// 对照 ui_kits/desktop/dapp.jsx DSettings(精简):兑换入口 + 语言段控 +
|
// 三组:连接(开机自启/自动连接/智能分流/kill-switch 开关)· 外观(语言/深色)·
|
||||||
// 深色外观 + 协议 + 版本。开关组(开机自启/智能分流/Kill-switch)待 l10n 补齐后加。
|
// 关于(协议/检查更新/版本 + Web 用户中心链接)。购买/订单入口已归 Account 页。
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../core/responsive/form_factor.dart';
|
|
||||||
import '../l10n/app_text.dart';
|
import '../l10n/app_text.dart';
|
||||||
import '../pangolin_theme.dart';
|
import '../pangolin_theme.dart';
|
||||||
import '../services/web_launch.dart';
|
import '../services/web_launch.dart';
|
||||||
import '../state/app_providers.dart';
|
import '../state/app_providers.dart';
|
||||||
import '../state/navigation_provider.dart';
|
|
||||||
import '../state/settings_provider.dart';
|
import '../state/settings_provider.dart';
|
||||||
import '../state/update_provider.dart';
|
import '../state/update_provider.dart';
|
||||||
import '../widgets/pangolin_icons.dart';
|
import '../widgets/pangolin_icons.dart';
|
||||||
import '../widgets/pangolin_toast.dart';
|
import '../widgets/pangolin_toast.dart';
|
||||||
import 'orders_page.dart';
|
|
||||||
import 'payment_page.dart';
|
|
||||||
import 'purchase_page.dart';
|
|
||||||
/// 「检查更新」手动触发:拉取 `$kApiBaseUrl/version`,失败/无更新走轻提示,
|
/// 「检查更新」手动触发:拉取 `$kApiBaseUrl/version`,失败/无更新走轻提示,
|
||||||
/// 有更新则清掉「已忽略版本」→ 顶部更新 banner 显示 + toast(不弹窗)。
|
/// 有更新则清掉「已忽略版本」→ 顶部更新 banner 显示 + toast(不弹窗)。
|
||||||
Future<void> _checkForUpdate(BuildContext context, WidgetRef ref, AppText t) async {
|
Future<void> _checkForUpdate(BuildContext context, WidgetRef ref, AppText t) async {
|
||||||
@@ -56,39 +52,13 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
Widget sw(bool v, ValueChanged<bool> on) =>
|
Widget sw(bool v, ValueChanged<bool> on) =>
|
||||||
Switch(value: v, activeThumbColor: c.accent, onChanged: 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(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(32, 16, 32, 24),
|
padding: const EdgeInsets.fromLTRB(32, 16, 32, 24),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||||
// 购买套餐 + 我的订单
|
// ① 连接(功能开关组;开机自启是桌面端登录项概念,iOS/Android 无此能力,仅桌面显示)
|
||||||
_Card(children: [
|
_SectionLabel(text: t.settingsGroupConn),
|
||||||
_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 无此能力,仅桌面显示)
|
|
||||||
_Card(children: [
|
_Card(children: [
|
||||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux)
|
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux)
|
||||||
_Row(title: t.autostart, sub: t.autostartSub, right: sw(s.autostart, settings.setAutostart)),
|
_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.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)),
|
_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: [
|
_Card(children: [
|
||||||
_Row(title: t.language, right: _LangSwitch(lang: lang, onPick: (l) => ref.read(localeProvider.notifier).set(l))),
|
_Row(title: t.language, right: _LangSwitch(lang: lang, onPick: (l) => ref.read(localeProvider.notifier).set(l))),
|
||||||
_Row(
|
_Row(
|
||||||
title: t.darkAppearance,
|
title: t.darkAppearance,
|
||||||
sub: isDark ? t.stateOn : t.followLight,
|
sub: isDark ? t.stateOn : t.followLight,
|
||||||
|
last: true,
|
||||||
right: sw(isDark, (v) => ref.read(themeModeProvider.notifier).state = v ? ThemeMode.dark : ThemeMode.light),
|
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.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: 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 {
|
class _Card extends StatelessWidget {
|
||||||
const _Card({required this.children});
|
const _Card({required this.children});
|
||||||
final List<Widget> children;
|
final List<Widget> children;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
@@ -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<void> saveTokens({required String access, required String refresh}) async {}
|
||||||
|
@override
|
||||||
|
Future<String?> loadAccessToken() async => null;
|
||||||
|
@override
|
||||||
|
Future<String?> loadRefreshToken() async => null;
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {}
|
||||||
|
@override
|
||||||
|
Future<void> markOnboarded() async {}
|
||||||
|
@override
|
||||||
|
Future<bool> isOnboarded() async => true;
|
||||||
|
@override
|
||||||
|
Future<void> saveLastEmail(String email) async {}
|
||||||
|
@override
|
||||||
|
Future<String?> 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user