refactor(client): SubScaffold 六合一(视觉零变化)+ tablet 注释订正 + 平板通知铃入口(#20)
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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<PaymentScreen> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PurchaseScreen> {
|
||||
},
|
||||
));
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = <NavSidebarItem>[
|
||||
(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()),
|
||||
]),
|
||||
),
|
||||
|
||||
@@ -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<RedeemScreen> {
|
||||
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: [
|
||||
|
||||
@@ -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),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |