diff --git a/client/lib/screens/invite_page.dart b/client/lib/screens/invite_page.dart index 0b2c0c9..0dd044e 100644 --- a/client/lib/screens/invite_page.dart +++ b/client/lib/screens/invite_page.dart @@ -16,6 +16,7 @@ import '../widgets/page_body.dart'; import '../widgets/pangolin_button.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/pangolin_toast.dart'; +import '../widgets/sub_scaffold.dart'; class InviteScreen extends ConsumerWidget { const InviteScreen({super.key, required this.t, this.onBack, this.embedded = false}); @@ -59,7 +60,6 @@ class InviteScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final c = context.pangolin; final inviteAsync = ref.watch(inviteProvider); Widget body = PageBody(child: inviteAsync.when( @@ -70,24 +70,11 @@ class InviteScreen extends ConsumerWidget { : _InviteContent(t: t, info: info, onCopy: _copy, onVerifyAndClaim: (ctx) => _verifyAndClaim(ctx, ref)), )); - if (embedded) return body; - return Scaffold( - backgroundColor: c.bg, - body: SafeArea( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), - child: Row(children: [ - IconButton( - onPressed: onBack ?? () => Navigator.of(context).maybePop(), - icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), - ), - Text(t.inviteTitle, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), - ]), - ), - Expanded(child: body), - ]), - ), + return SubScaffold( + title: t.inviteTitle, + onBack: onBack, + embedded: embedded, + child: body, ); } } diff --git a/client/lib/screens/notifications_page.dart b/client/lib/screens/notifications_page.dart index bc26561..e87b0df 100644 --- a/client/lib/screens/notifications_page.dart +++ b/client/lib/screens/notifications_page.dart @@ -7,9 +7,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_text.dart'; import '../pangolin_theme.dart'; -import '../widgets/page_body.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/status_pill.dart'; +import '../widgets/sub_scaffold.dart'; typedef _Notice = ({ IconData icon, @@ -137,24 +137,12 @@ class NotificationsScreen extends ConsumerWidget { ], ); - if (embedded) return PageBody(child: content()); - return Scaffold( - backgroundColor: c.bg, - body: SafeArea( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), - child: Row(children: [ - IconButton( - onPressed: onBack ?? () => Navigator.of(context).maybePop(), - icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), - ), - Text(t.notifTitle, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), - ]), - ), - Expanded(child: PageBody(child: content())), - ]), - ), + return SubScaffold( + title: t.notifTitle, + onBack: onBack, + embedded: embedded, + wrapPageBody: true, + child: content(), ); } } diff --git a/client/lib/screens/orders_page.dart b/client/lib/screens/orders_page.dart index e3b743f..10b2445 100644 --- a/client/lib/screens/orders_page.dart +++ b/client/lib/screens/orders_page.dart @@ -14,6 +14,7 @@ import '../pangolin_theme.dart'; import '../state/navigation_provider.dart'; import '../state/orders_provider.dart'; import '../widgets/page_body.dart'; +import '../widgets/sub_scaffold.dart'; import '../widgets/pangolin_button.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/status_pill.dart'; @@ -58,38 +59,6 @@ String _fmtDateTime(DateTime d) { return '${l.year}-${two(l.month)}-${two(l.day)} ${two(l.hour)}:${two(l.minute)}'; } -/// 带返回栏的子页骨架(embedded=true 只返回内容,返回/标题由外层 shell 顶栏提供)。 -class _SubScaffold extends StatelessWidget { - 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 PageBody(child: child); - return Scaffold( - backgroundColor: c.bg, - body: SafeArea( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), - child: Row(children: [ - IconButton( - onPressed: onBack ?? () => Navigator.of(context).maybePop(), - icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), - ), - Text(title, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), - ]), - ), - Expanded(child: PageBody(child: child)), - ]), - ), - ); - } -} - /// 订单页入口:内部在「列表」与「详情」两屏间切换(与原型单帧同构)。 class OrdersScreen extends ConsumerStatefulWidget { const OrdersScreen({super.key, required this.t, this.onBack, this.embedded = false}); @@ -196,7 +165,7 @@ class _OrderListView extends ConsumerWidget { ]), ); - return _SubScaffold(title: t.ordersTitle, onBack: onBack, embedded: embedded, child: content()); + return SubScaffold(title: t.ordersTitle, onBack: onBack, embedded: embedded, wrapPageBody: true, child: content()); } Widget _row(BuildContext context, PangolinScheme c, PayOrderSummary o, bool divider) { @@ -289,7 +258,7 @@ class _OrderDetailView extends ConsumerWidget { final expiresAt = detailAsync.valueOrNull?.expiresAt; final pill = _statusPill(t, o.status); - // 内容区(mobile 套 _SubScaffold;desktop embedded 前置一条返回条回到列表)。 + // 内容区(mobile 套 SubScaffold;desktop embedded 前置一条返回条回到列表)。 Widget summaryCard() => Container( width: double.infinity, decoration: BoxDecoration( @@ -376,7 +345,7 @@ class _OrderDetailView extends ConsumerWidget { Expanded(child: PageBody(child: body())), ]); } - return _SubScaffold(title: t.orderDetailTitle, onBack: onBack, child: body()); + return SubScaffold(title: t.orderDetailTitle, onBack: onBack, wrapPageBody: true, child: body()); } Widget _kv(PangolinScheme c, String label, String value, {VoidCallback? onCopy}) { diff --git a/client/lib/screens/payment_page.dart b/client/lib/screens/payment_page.dart index a623b90..c8a339c 100644 --- a/client/lib/screens/payment_page.dart +++ b/client/lib/screens/payment_page.dart @@ -15,6 +15,7 @@ import '../state/payment_provider.dart'; import '../widgets/pangolin_button.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/pangolin_toast.dart'; +import '../widgets/sub_scaffold.dart'; class PaymentScreen extends ConsumerStatefulWidget { const PaymentScreen({super.key, required this.t, this.onBack, this.onDone, this.embedded = false}); @@ -311,25 +312,11 @@ class _PaymentScreenState extends ConsumerState { style: PangolinText.body.copyWith(color: c.fg3))), }; - if (embedded) return body; - return Scaffold( - backgroundColor: c.bg, - body: SafeArea( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), - child: Row(children: [ - IconButton( - onPressed: onBack ?? () => Navigator.of(context).maybePop(), - icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), - ), - Text(t.paymentTitle, - style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), - ]), - ), - Expanded(child: body), - ]), - ), + return SubScaffold( + title: t.paymentTitle, + onBack: onBack, + embedded: embedded, + child: body, ); } } diff --git a/client/lib/screens/purchase_page.dart b/client/lib/screens/purchase_page.dart index b04c70c..9317bb7 100644 --- a/client/lib/screens/purchase_page.dart +++ b/client/lib/screens/purchase_page.dart @@ -11,6 +11,7 @@ import '../pangolin_theme.dart'; import '../state/payment_provider.dart'; import '../widgets/page_body.dart'; import '../widgets/pangolin_icons.dart'; +import '../widgets/sub_scaffold.dart'; import '../widgets/pangolin_toast.dart'; import '../widgets/plan_card.dart'; import '../widgets/seg_switch.dart'; @@ -133,25 +134,11 @@ class _PurchaseScreenState extends ConsumerState { }, )); - if (widget.embedded) return body; - return Scaffold( - backgroundColor: c.bg, - body: SafeArea( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), - child: Row(children: [ - IconButton( - onPressed: widget.onBack ?? () => Navigator.of(context).maybePop(), - icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), - ), - Text(t.purchaseTitle, - style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), - ]), - ), - Expanded(child: body), - ]), - ), + return SubScaffold( + title: t.purchaseTitle, + onBack: widget.onBack, + embedded: widget.embedded, + child: body, ); } } diff --git a/client/lib/shell/tablet_shell.dart b/client/lib/shell/tablet_shell.dart index 001c095..522e1b2 100644 --- a/client/lib/shell/tablet_shell.dart +++ b/client/lib/shell/tablet_shell.dart @@ -10,6 +10,7 @@ import '../screens/account_page.dart'; import '../screens/connect_page.dart'; import '../screens/contact_page.dart'; import '../screens/nodes_page.dart'; +import '../screens/notifications_page.dart'; import '../screens/settings_page.dart'; import '../screens/stats_page.dart'; import '../state/app_providers.dart'; @@ -27,7 +28,7 @@ class TabletShell extends ConsumerWidget { final t = ref.watch(appTextProvider); final view = ref.watch(navViewProvider); - // 「我的」移到侧栏顶部品牌按钮(见 NavSidebar);设置/联系我们对齐桌面侧栏。 + // 「我的」经侧栏底部 PlanBadgeCard 进入(见 NavSidebar;品牌块本身不可点);设置/联系我们对齐桌面侧栏。 final items = [ (icon: PangolinIcons.power, label: t.tabConnect, view: NavView.connect), (icon: PangolinIcons.globe, label: t.tabServers, view: NavView.servers), @@ -61,11 +62,19 @@ class TabletShell extends ConsumerWidget { return const SettingsPage(); case NavView.contact: return const ContactPage(); + case NavView.notifications: + return NotificationsScreen(t: t, embedded: true); default: // account / plans / redeem / devices → 账户(其余下钻在账户页内) return const AccountPage(isWide: true); } } + // 通知子页返回目标:回账户(镜像桌面 desktop_shell.dart::backTarget)。 + VoidCallback? backTarget() { + if (view == NavView.notifications) return () => go(NavView.account); + return null; + } + return ColoredBox( color: c.bg, // iPad 上内容会顶到状态栏(时间/电量/notch)。SafeArea 加顶部安全区,避开重叠; @@ -76,7 +85,13 @@ class TabletShell extends ConsumerWidget { NavSidebar(items: items, width: 232, dense: false), Expanded( child: Column(children: [ - ContentTopBar(title: titles[view] ?? t.tabMe, showThemeToggle: false, titleSize: 19), + ContentTopBar( + title: titles[view] ?? t.tabMe, + onBack: backTarget(), + onBell: () => go(NavView.notifications), + showThemeToggle: false, + titleSize: 19, + ), Expanded(child: content()), ]), ), diff --git a/client/lib/widgets/account_screens.dart b/client/lib/widgets/account_screens.dart index 583efcd..2497eab 100644 --- a/client/lib/widgets/account_screens.dart +++ b/client/lib/widgets/account_screens.dart @@ -20,39 +20,7 @@ import '../services/auth_api.dart'; import '../state/account_providers.dart'; import 'pangolin_button.dart'; import 'pangolin_icons.dart'; - -/// 通用:带返回箭头的子页骨架。embedded=true 时只返回内容(desktop 内容区下钻, -/// 返回/标题由外层 shell 顶栏提供),否则整页 Scaffold(mobile/tablet 全屏 push)。 -class _SubScaffold extends StatelessWidget { - 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( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), - child: Row(children: [ - IconButton( - onPressed: onBack ?? () => Navigator.of(context).maybePop(), - icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), - ), - Text(title, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), - ]), - ), - Expanded(child: child), - ]), - ), - ); - } -} +import 'sub_scaffold.dart'; /// ── 设置(移动端从「我的」下钻)── 复用桌面 SettingsPage 内容,套移动返回栏。 class SettingsScreen extends StatelessWidget { @@ -62,7 +30,7 @@ class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return _SubScaffold(title: t.settingsTitle, onBack: onBack, child: const SettingsPage()); + return SubScaffold(title: t.settingsTitle, onBack: onBack, child: const SettingsPage()); } } @@ -113,7 +81,7 @@ class DevicesScreen extends ConsumerWidget { ]), ); - return _SubScaffold( + return SubScaffold( title: t.myDevices, onBack: onBack, embedded: embedded, child: _DevicesAutoRefresh(child: content())); } @@ -314,7 +282,7 @@ class _RedeemScreenState extends ConsumerState { Widget build(BuildContext context) { final c = context.pangolin; final t = widget.t; - return _SubScaffold( + return SubScaffold( title: t.redeemTitle, onBack: widget.onBack, embedded: widget.embedded, @@ -379,7 +347,7 @@ class ContactScreen extends StatelessWidget { accent: ch.accent, )) .toList(); - return _SubScaffold( + return SubScaffold( title: t.contactTitle, onBack: onBack, child: ListView(padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), children: [ diff --git a/client/lib/widgets/sub_scaffold.dart b/client/lib/widgets/sub_scaffold.dart new file mode 100644 index 0000000..b62c389 --- /dev/null +++ b/client/lib/widgets/sub_scaffold.dart @@ -0,0 +1,57 @@ +// sub_scaffold.dart — 通用「返回箭头 + 标题」子页脚手架(六处收敛,视觉零变化)。 +// +// 收敛自 account_screens.dart/orders_page.dart 的私有 `_SubScaffold`,以及 +// purchase_page.dart/payment_page.dart/invite_page.dart/notifications_page.dart +// 里结构相同的内联等价实现。逐像素沿用原实现(同 padding/字号/图标),纯收敛非重设计。 +// +// embedded=true 时只返回内容(desktop 内容区下钻,返回/标题由外层 shell 顶栏提供); +// 否则整页 Scaffold + SafeArea + 返回箭头行(mobile/tablet 全屏 push)。 +// wrapPageBody=true 时由本组件把 child 套进 PageBody(桌面内容页统一宽度容器);默认 +// false——调用方若已自行用 PageBody 包裹 child(如购买页需要 wide 变体),避免重复包裹。 +import 'package:flutter/material.dart'; + +import '../pangolin_theme.dart'; +import 'page_body.dart'; +import 'pangolin_icons.dart'; + +class SubScaffold extends StatelessWidget { + const SubScaffold({ + super.key, + required this.title, + required this.child, + this.onBack, + this.embedded = false, + this.wrapPageBody = false, + }); + + final String title; + final Widget child; + final VoidCallback? onBack; + final bool embedded; + final bool wrapPageBody; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final body = wrapPageBody ? PageBody(child: child) : child; + if (embedded) return body; + return Scaffold( + backgroundColor: c.bg, + body: SafeArea( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), + child: Row(children: [ + IconButton( + onPressed: onBack ?? () => Navigator.of(context).maybePop(), + icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), + ), + Text(title, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), + ]), + ), + Expanded(child: body), + ]), + ), + ); + } +} diff --git a/client/test/golden/goldens/tablet_account_dark_zh.png b/client/test/golden/goldens/tablet_account_dark_zh.png index 0302dba..255e347 100644 Binary files a/client/test/golden/goldens/tablet_account_dark_zh.png and b/client/test/golden/goldens/tablet_account_dark_zh.png differ diff --git a/client/test/golden/goldens/tablet_account_light_en.png b/client/test/golden/goldens/tablet_account_light_en.png index 7a21f6b..d623e45 100644 Binary files a/client/test/golden/goldens/tablet_account_light_en.png and b/client/test/golden/goldens/tablet_account_light_en.png differ diff --git a/client/test/golden/goldens/tablet_account_light_zh.png b/client/test/golden/goldens/tablet_account_light_zh.png index 2adefc4..3072180 100644 Binary files a/client/test/golden/goldens/tablet_account_light_zh.png and b/client/test/golden/goldens/tablet_account_light_zh.png differ diff --git a/client/test/golden/goldens/tablet_connect_dark_zh.png b/client/test/golden/goldens/tablet_connect_dark_zh.png index ba1e346..dba1d1c 100644 Binary files a/client/test/golden/goldens/tablet_connect_dark_zh.png and b/client/test/golden/goldens/tablet_connect_dark_zh.png differ diff --git a/client/test/golden/goldens/tablet_connect_light_en.png b/client/test/golden/goldens/tablet_connect_light_en.png index 7075043..fc34b41 100644 Binary files a/client/test/golden/goldens/tablet_connect_light_en.png and b/client/test/golden/goldens/tablet_connect_light_en.png differ diff --git a/client/test/golden/goldens/tablet_connect_light_zh.png b/client/test/golden/goldens/tablet_connect_light_zh.png index 2ca7ca8..425f27f 100644 Binary files a/client/test/golden/goldens/tablet_connect_light_zh.png and b/client/test/golden/goldens/tablet_connect_light_zh.png differ diff --git a/client/test/golden/goldens/tablet_servers_dark_zh.png b/client/test/golden/goldens/tablet_servers_dark_zh.png index 6b7f6d3..4914eef 100644 Binary files a/client/test/golden/goldens/tablet_servers_dark_zh.png and b/client/test/golden/goldens/tablet_servers_dark_zh.png differ diff --git a/client/test/golden/goldens/tablet_servers_light_en.png b/client/test/golden/goldens/tablet_servers_light_en.png index fbef899..48da3f4 100644 Binary files a/client/test/golden/goldens/tablet_servers_light_en.png and b/client/test/golden/goldens/tablet_servers_light_en.png differ diff --git a/client/test/golden/goldens/tablet_servers_light_zh.png b/client/test/golden/goldens/tablet_servers_light_zh.png index e77dd61..6d1e623 100644 Binary files a/client/test/golden/goldens/tablet_servers_light_zh.png and b/client/test/golden/goldens/tablet_servers_light_zh.png differ diff --git a/client/test/golden/goldens/tablet_stats_dark_zh.png b/client/test/golden/goldens/tablet_stats_dark_zh.png index 3363894..0a7c1a1 100644 Binary files a/client/test/golden/goldens/tablet_stats_dark_zh.png and b/client/test/golden/goldens/tablet_stats_dark_zh.png differ diff --git a/client/test/golden/goldens/tablet_stats_light_en.png b/client/test/golden/goldens/tablet_stats_light_en.png index 5891641..e1a9c43 100644 Binary files a/client/test/golden/goldens/tablet_stats_light_en.png and b/client/test/golden/goldens/tablet_stats_light_en.png differ diff --git a/client/test/golden/goldens/tablet_stats_light_zh.png b/client/test/golden/goldens/tablet_stats_light_zh.png index e24035f..521e72d 100644 Binary files a/client/test/golden/goldens/tablet_stats_light_zh.png and b/client/test/golden/goldens/tablet_stats_light_zh.png differ