design+client: 抽 .view-body/PageBody 统一桌面内容页宽度(600/wide 840)——stats·contact 补约束,settings·contact 560→600,九页收口
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
@@ -12,6 +12,7 @@ import '../state/navigation_provider.dart';
|
||||
import '../widgets/account_screens.dart';
|
||||
import '../widgets/app_top_bar.dart';
|
||||
import '../widgets/notification_bell.dart';
|
||||
import '../widgets/page_body.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
import 'invite_page.dart';
|
||||
import 'notifications_page.dart';
|
||||
@@ -50,7 +51,7 @@ class AccountPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
final body = ListView(
|
||||
final body = PageBody(child: ListView(
|
||||
padding: EdgeInsets.fromLTRB(pad, isWide ? 6 : 0, pad, 24),
|
||||
children: [
|
||||
_PlanBanner(
|
||||
@@ -127,7 +128,7 @@ class AccountPage extends ConsumerWidget {
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
if (isWide) return body;
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../pangolin_theme.dart';
|
||||
import '../services/channel_launch.dart';
|
||||
import '../services/contact_channels.dart';
|
||||
import '../state/app_providers.dart';
|
||||
import '../widgets/page_body.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
|
||||
class ContactPage extends ConsumerWidget {
|
||||
@@ -36,8 +37,7 @@ class ContactPage extends ConsumerWidget {
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(32, 16, 32, 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: PageBody(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
Text(t.contactIntro, style: PangolinText.sm.copyWith(color: c.fg2)),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../pangolin_theme.dart';
|
||||
import '../services/channel_launch.dart';
|
||||
import '../services/invite_api.dart';
|
||||
import '../state/invite_provider.dart';
|
||||
import '../widgets/page_body.dart';
|
||||
import '../widgets/pangolin_button.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
import '../widgets/pangolin_toast.dart';
|
||||
@@ -61,13 +62,13 @@ class InviteScreen extends ConsumerWidget {
|
||||
final c = context.pangolin;
|
||||
final inviteAsync = ref.watch(inviteProvider);
|
||||
|
||||
Widget body = inviteAsync.when(
|
||||
Widget body = PageBody(child: inviteAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => _Placeholder(t: t),
|
||||
data: (info) => info == null
|
||||
? _Placeholder(t: t)
|
||||
: _InviteContent(t: t, info: info, onCopy: _copy, onVerifyAndClaim: (ctx) => _verifyAndClaim(ctx, ref)),
|
||||
);
|
||||
));
|
||||
|
||||
if (embedded) return body;
|
||||
return Scaffold(
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
|
||||
@@ -136,7 +137,7 @@ class NotificationsScreen extends ConsumerWidget {
|
||||
],
|
||||
);
|
||||
|
||||
if (embedded) return content();
|
||||
if (embedded) return PageBody(child: content());
|
||||
return Scaffold(
|
||||
backgroundColor: c.bg,
|
||||
body: SafeArea(
|
||||
@@ -151,7 +152,7 @@ class NotificationsScreen extends ConsumerWidget {
|
||||
Text(t.notifTitle, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)),
|
||||
]),
|
||||
),
|
||||
Expanded(child: content()),
|
||||
Expanded(child: PageBody(child: content())),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../models/payment.dart';
|
||||
import '../pangolin_theme.dart';
|
||||
import '../state/navigation_provider.dart';
|
||||
import '../state/orders_provider.dart';
|
||||
import '../widgets/page_body.dart';
|
||||
import '../widgets/pangolin_button.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
import '../widgets/status_pill.dart';
|
||||
@@ -67,7 +68,7 @@ class _SubScaffold extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = context.pangolin;
|
||||
if (embedded) return child;
|
||||
if (embedded) return PageBody(child: child);
|
||||
return Scaffold(
|
||||
backgroundColor: c.bg,
|
||||
body: SafeArea(
|
||||
@@ -82,7 +83,7 @@ class _SubScaffold extends StatelessWidget {
|
||||
Text(title, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)),
|
||||
]),
|
||||
),
|
||||
Expanded(child: child),
|
||||
Expanded(child: PageBody(child: child)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -362,7 +363,7 @@ class _OrderDetailView extends ConsumerWidget {
|
||||
]),
|
||||
),
|
||||
),
|
||||
Expanded(child: body()),
|
||||
Expanded(child: PageBody(child: body())),
|
||||
]);
|
||||
}
|
||||
return _SubScaffold(title: t.orderDetailTitle, onBack: onBack, child: body());
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../l10n/app_text.dart';
|
||||
import '../models/payment.dart';
|
||||
import '../pangolin_theme.dart';
|
||||
import '../state/payment_provider.dart';
|
||||
import '../widgets/page_body.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
import '../widgets/pangolin_toast.dart';
|
||||
import '../widgets/plan_card.dart';
|
||||
@@ -70,7 +71,7 @@ class _PurchaseScreenState extends ConsumerState<PurchaseScreen> {
|
||||
final c = context.pangolin;
|
||||
final catalog = ref.watch(payCatalogProvider);
|
||||
|
||||
final body = catalog.when(
|
||||
final body = PageBody(wide: true, child: catalog.when(
|
||||
loading: () => const Center(
|
||||
child: Padding(padding: EdgeInsets.all(40), child: CircularProgressIndicator())),
|
||||
error: (_, __) => Center(
|
||||
@@ -132,7 +133,7 @@ class _PurchaseScreenState extends ConsumerState<PurchaseScreen> {
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
));
|
||||
|
||||
if (widget.embedded) return body;
|
||||
return Scaffold(
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../services/web_launch.dart';
|
||||
import '../state/app_providers.dart';
|
||||
import '../state/settings_provider.dart';
|
||||
import '../state/update_provider.dart';
|
||||
import '../widgets/page_body.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
import '../widgets/pangolin_toast.dart';
|
||||
|
||||
@@ -54,8 +55,7 @@ class SettingsPage extends ConsumerWidget {
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(32, 16, 32, 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: PageBody(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
// ① 连接(功能开关组;开机自启是桌面端登录项概念,iOS/Android 无此能力,仅桌面显示)
|
||||
_SectionLabel(text: t.settingsGroupConn),
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../state/account_providers.dart';
|
||||
import '../state/app_providers.dart';
|
||||
import '../state/auth_provider.dart';
|
||||
import '../widgets/app_top_bar.dart';
|
||||
import '../widgets/page_body.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
import '../widgets/period_card.dart';
|
||||
import '../widgets/usage_line_chart.dart';
|
||||
@@ -69,7 +70,7 @@ class StatsPage extends ConsumerWidget {
|
||||
ref.invalidate(deviceUsageProvider(30));
|
||||
await ref.read(meProvider.notifier).refresh();
|
||||
},
|
||||
child: ListView(
|
||||
child: PageBody(child: ListView(
|
||||
padding: EdgeInsets.fromLTRB(isWide ? 32 : 20, isWide ? 8 : 4, isWide ? 32 : 20, 24),
|
||||
children: [
|
||||
// 顶行:标题(宽屏) + 设备下拉(右上角)
|
||||
@@ -115,7 +116,7 @@ class StatsPage extends ConsumerWidget {
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
if (isWide) return body;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// page_body.dart — 桌面内容页统一宽度容器(镜像 design/prototype/atoms.css 的 .view-body)。
|
||||
// 默认 600 居中;wide=true 840(多列网格页,如购买页)。手机屏宽 < 600 时约束自然不生效,可无条件包。
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
const double kViewMaxW = 600; // atoms.css .view-body
|
||||
const double kViewMaxWWide = 840; // atoms.css .view-body.is-wide
|
||||
|
||||
class PageBody extends StatelessWidget {
|
||||
const PageBody({super.key, required this.child, this.wide = false});
|
||||
final Widget child;
|
||||
final bool wide;
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: wide ? kViewMaxWWide : kViewMaxW),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 62 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 |