diff --git a/client/lib/screens/account_page.dart b/client/lib/screens/account_page.dart index 7e34cde..2188129 100644 --- a/client/lib/screens/account_page.dart +++ b/client/lib/screens/account_page.dart @@ -2,9 +2,11 @@ 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 '../state/app_providers.dart'; +import '../state/navigation_provider.dart'; import '../widgets/account_screens.dart'; import '../widgets/app_top_bar.dart'; import '../widgets/pangolin_icons.dart'; @@ -24,13 +26,20 @@ class AccountPage extends ConsumerWidget { final isDark = Theme.of(context).brightness == Brightness.dark; final pad = isWide ? 28.0 : 20.0; - void push(Widget page) => - Navigator.of(context).push(MaterialPageRoute(builder: (_) => page)); + // desktop 在内容区下钻(切 navView);mobile/tablet 全屏 push。 + 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)); + } + } final body = ListView( padding: EdgeInsets.fromLTRB(pad, isWide ? 6 : 0, pad, 24), children: [ - _PlanBanner(t: t, isFree: isFree, onUpgrade: () => push(PlansScreen(t: t))), + _PlanBanner(t: t, isFree: isFree, onUpgrade: () => open(NavView.plans, PlansScreen(t: t))), const SizedBox(height: 18), // 账户信息 _SectionLabel(text: t.accInfoTitle), @@ -42,11 +51,11 @@ class AccountPage extends ConsumerWidget { const SizedBox(height: 16), // 设备 / 兑换 / 联系 _Card(children: [ - _NavRow(icon: PangolinIcons.monitorSmartphone, title: t.myDevices, onTap: () => push(DevicesScreen(t: t))), + _NavRow(icon: PangolinIcons.monitorSmartphone, title: t.myDevices, onTap: () => open(NavView.devices, DevicesScreen(t: t))), Divider(height: 1, color: c.border), - _NavRow(icon: PangolinIcons.shoppingBag, title: t.redeemEntry, onTap: () => push(RedeemScreen(t: t))), + _NavRow(icon: PangolinIcons.shoppingBag, title: t.redeemEntry, onTap: () => open(NavView.redeem, RedeemScreen(t: t))), Divider(height: 1, color: c.border), - _NavRow(icon: PangolinIcons.messageCircle, title: t.contactEntry, onTap: () => push(ContactScreen(t: t))), + _NavRow(icon: PangolinIcons.messageCircle, title: t.contactEntry, onTap: () => open(NavView.contact, ContactScreen(t: t))), ]), const SizedBox(height: 16), // 语言 / 主题 / 协议 diff --git a/client/lib/shell/desktop_shell.dart b/client/lib/shell/desktop_shell.dart index 2829612..a5e9547 100644 --- a/client/lib/shell/desktop_shell.dart +++ b/client/lib/shell/desktop_shell.dart @@ -13,6 +13,7 @@ import '../screens/settings_page.dart'; import '../screens/stats_page.dart'; import '../state/app_providers.dart'; import '../state/navigation_provider.dart'; +import '../widgets/account_screens.dart'; import '../widgets/content_top_bar.dart'; import '../widgets/nav_sidebar.dart'; import '../widgets/pangolin_icons.dart'; @@ -42,6 +43,9 @@ class DesktopShell extends ConsumerWidget { NavView.account: t.tabMe, NavView.contact: t.contactTitle, NavView.settings: t.settingsTitle, + NavView.plans: t.choosePlan, + NavView.redeem: t.redeemTitle, + NavView.devices: t.myDevices, }; void go(NavView v) => ref.read(navViewProvider.notifier).state = v; @@ -60,19 +64,27 @@ class DesktopShell extends ConsumerWidget { return const ContactPage(); case NavView.settings: return const SettingsPage(); + // 账户下钻子页(内容区,顶栏带返回) case NavView.plans: + return PlansScreen(t: t, embedded: true, onChoose: (_) => go(NavView.redeem)); case NavView.redeem: - return const AccountPage(isWide: true); + return RedeemScreen(t: t, embedded: true); + case NavView.devices: + return DevicesScreen(t: t, embedded: true); } } + final isSub = kAccountSubViews.contains(view); return ColoredBox( color: c.bg, child: Row(children: [ NavSidebar(items: items), Expanded( child: Column(children: [ - ContentTopBar(title: titles[view] ?? t.brand), + ContentTopBar( + title: titles[view] ?? t.brand, + onBack: isSub ? () => go(NavView.account) : null, + ), Expanded(child: content()), ]), ), diff --git a/client/lib/state/navigation_provider.dart b/client/lib/state/navigation_provider.dart index 0cd5cbb..a9fa899 100644 --- a/client/lib/state/navigation_provider.dart +++ b/client/lib/state/navigation_provider.dart @@ -4,7 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// 一级视图 + 账户子页。 /// desktop 侧栏暴露 6 个一级项(connect..settings);mobile/tablet 取前 4 项, /// contact/settings 在 mobile 走账户子页。plans/redeem 是 account 的下钻页。 -enum NavView { connect, servers, stats, account, contact, settings, plans, redeem } +enum NavView { connect, servers, stats, account, contact, settings, plans, redeem, devices } + +/// 账户下钻子页(desktop 在内容区切换、顶栏带返回;mobile/tablet 走全屏 push)。 +const Set kAccountSubViews = {NavView.plans, NavView.redeem, NavView.devices}; /// 当前视图。 final navViewProvider = StateProvider((ref) => NavView.connect); diff --git a/client/lib/widgets/account_screens.dart b/client/lib/widgets/account_screens.dart index 91b1147..ebf1a2e 100644 --- a/client/lib/widgets/account_screens.dart +++ b/client/lib/widgets/account_screens.dart @@ -10,15 +10,18 @@ import 'pangolin_button.dart'; import 'pangolin_icons.dart'; import 'plan_card.dart'; -/// 通用:带返回箭头的子页骨架 +/// 通用:带返回箭头的子页骨架。embedded=true 时只返回内容(desktop 内容区下钻, +/// 返回/标题由外层 shell 顶栏提供),否则整页 Scaffold(mobile/tablet 全屏 push)。 class _SubScaffold extends StatelessWidget { - const _SubScaffold({required this.title, required this.child, this.onBack}); + const _SubScaffold({required this.title, required this.child, this.onBack, this.embedded = false}); final String title; final Widget child; final VoidCallback? onBack; + final bool embedded; @override Widget build(BuildContext context) { final c = context.pangolin; + if (embedded) return child; return Scaffold( backgroundColor: c.bg, body: SafeArea( @@ -42,10 +45,11 @@ class _SubScaffold extends StatelessWidget { /// ── 套餐选择 ── class PlansScreen extends StatelessWidget { - const PlansScreen({super.key, required this.t, this.onChoose, this.onBack}); + const PlansScreen({super.key, required this.t, this.onChoose, this.onBack, this.embedded = false}); final AppText t; final ValueChanged? onChoose; final VoidCallback? onBack; + final bool embedded; @override Widget build(BuildContext context) { @@ -57,6 +61,7 @@ class PlansScreen extends StatelessWidget { return _SubScaffold( title: t.choosePlan, onBack: onBack, + embedded: embedded, child: ListView( padding: const EdgeInsets.fromLTRB(20, 14, 20, 24), children: [ @@ -84,9 +89,10 @@ class DeviceItem { } class DevicesScreen extends StatefulWidget { - const DevicesScreen({super.key, required this.t, this.onBack}); + const DevicesScreen({super.key, required this.t, this.onBack, this.embedded = false}); final AppText t; final VoidCallback? onBack; + final bool embedded; @override State createState() => _DevicesScreenState(); } @@ -104,6 +110,7 @@ class _DevicesScreenState extends State { return _SubScaffold( title: widget.t.myDevices, onBack: widget.onBack, + embedded: widget.embedded, child: ListView(padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), children: [ Text(widget.t.devicesSub, style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400)), const SizedBox(height: 12), @@ -152,9 +159,10 @@ class _DevicesScreenState extends State { /// ── 兑换 & 购买 ── class RedeemScreen extends StatefulWidget { - const RedeemScreen({super.key, required this.t, this.onBack}); + const RedeemScreen({super.key, required this.t, this.onBack, this.embedded = false}); final AppText t; final VoidCallback? onBack; + final bool embedded; @override State createState() => _RedeemScreenState(); } @@ -182,6 +190,7 @@ class _RedeemScreenState extends State { return _SubScaffold( title: t.redeemTitle, onBack: widget.onBack, + embedded: widget.embedded, child: ListView(padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), children: [ Container( padding: const EdgeInsets.all(18), diff --git a/client/test/golden/desktop_pages_golden_test.dart b/client/test/golden/desktop_pages_golden_test.dart index b2b7595..078e564 100644 --- a/client/test/golden/desktop_pages_golden_test.dart +++ b/client/test/golden/desktop_pages_golden_test.dart @@ -61,4 +61,8 @@ void main() { testWidgets('desktop 账户页', (t) => _shoot(t, NavView.account, 'desktop_account')); testWidgets('desktop 联系页', (t) => _shoot(t, NavView.contact, 'desktop_contact')); testWidgets('desktop 设置页', (t) => _shoot(t, NavView.settings, 'desktop_settings')); + // 账户下钻子页(内容区 + 顶栏返回) + testWidgets('desktop 套餐选择', (t) => _shoot(t, NavView.plans, 'desktop_plans')); + testWidgets('desktop 兑换购买', (t) => _shoot(t, NavView.redeem, 'desktop_redeem')); + testWidgets('desktop 设备管理', (t) => _shoot(t, NavView.devices, 'desktop_devices')); } diff --git a/client/test/golden/goldens/desktop_devices.png b/client/test/golden/goldens/desktop_devices.png new file mode 100644 index 0000000..609f15e Binary files /dev/null and b/client/test/golden/goldens/desktop_devices.png differ diff --git a/client/test/golden/goldens/desktop_plans.png b/client/test/golden/goldens/desktop_plans.png new file mode 100644 index 0000000..d795048 Binary files /dev/null and b/client/test/golden/goldens/desktop_plans.png differ diff --git a/client/test/golden/goldens/desktop_redeem.png b/client/test/golden/goldens/desktop_redeem.png new file mode 100644 index 0000000..e4e97e3 Binary files /dev/null and b/client/test/golden/goldens/desktop_redeem.png differ