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
This commit is contained in:
wangjia
2026-07-13 09:46:57 +08:00
parent b88921a030
commit fa9a5c2d5e
22 changed files with 87 additions and 34 deletions
+3 -2
View File
@@ -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: [
+2 -2
View File
@@ -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),
+3 -2
View File
@@ -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(
+3 -2
View File
@@ -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())),
]),
),
);
+4 -3
View File
@@ -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());
+3 -2
View File
@@ -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(
+2 -2
View File
@@ -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),
+3 -2
View File
@@ -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;
+20
View File
@@ -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,
),
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

+10
View File
@@ -92,6 +92,16 @@
.btn-block { width: 100%; }
/* =============================================================
VIEW BODY · .view-body (+ .is-wide)
桌面客户端内容视图统一宽度容器 —— 对齐 Flutter PageBody(client/lib/widgets/page_body.dart)。
默认 600px 居中(单栏内容页:账户/设置/联系我们/统计/订单/通知/邀请);
.is-wide 840px(多列网格页:购买套餐)。宽度标量本身无对应 token,字面写死属正常。
============================================================= */
.view-body{ max-width:600px; margin:0 auto; padding:var(--space-5) var(--space-6) var(--space-6); }
.view-body.is-wide{ max-width:840px; }
/* =============================================================
CARD · .card
surface 底 + border + radius-lg + shadow-sm
+24
View File
@@ -189,6 +189,7 @@
<div class="dgroup">组件 Atoms</div>
<a class="dnav" data-s="a-btn">按钮 .btn</a>
<a class="dnav" data-s="a-card">卡片 .card</a>
<a class="dnav" data-s="a-viewbody">内容容器 .view-body</a>
<a class="dnav" data-s="a-input">输入 .input</a>
<a class="dnav" data-s="a-pill">胶囊 .pill / .badge</a>
<a class="dnav" data-s="a-lang">语言下拉 .langsel</a>
@@ -433,6 +434,29 @@
</div></div>
</section>
<section class="section" id="a-viewbody">
<h2>内容容器 · .view-body</h2>
<p class="desc">桌面客户端内容视图统一宽度容器:默认居中 <code>600px</code>(账户/设置/联系我们/统计/订单/通知/邀请等单栏页),<code>.is-wide</code> 修饰 <code>840px</code>(购买套餐等多列网格页)。padding 固定 <code>space-5 space-6 space-6</code>。对齐 Flutter <code>PageBody</code>(<code>client/lib/widgets/page_body.dart</code>)。</p>
<div class="pcard"><div class="pcard-h"><h3>默认 · 600px</h3><span class="tk">.view-body</span></div><div class="pcard-b col">
<div style="border:1px dashed var(--border-strong); border-radius:var(--radius-md); background:var(--bg-subtle)">
<div class="view-body" style="border:1px solid var(--accent-border); background:var(--surface); border-radius:var(--radius-md)">
<div class="t-overline" style="color:var(--fg3)">.view-body</div>
<div class="t-h3" style="margin:var(--space-2) 0 var(--space-1)">单栏内容视图</div>
<div class="t-sm" style="color:var(--fg2)">max-width:600px · margin:0 auto 居中 · padding 5/6/6</div>
</div>
</div>
</div></div>
<div class="pcard"><div class="pcard-h"><h3>宽 · 840px</h3><span class="tk">.view-body.is-wide</span></div><div class="pcard-b col">
<div style="border:1px dashed var(--border-strong); border-radius:var(--radius-md); background:var(--bg-subtle)">
<div class="view-body is-wide" style="border:1px solid var(--accent-border); background:var(--surface); border-radius:var(--radius-md)">
<div class="t-overline" style="color:var(--fg3)">.view-body.is-wide</div>
<div class="t-h3" style="margin:var(--space-2) 0 var(--space-1)">多列网格页(购买套餐)</div>
<div class="t-sm" style="color:var(--fg2)">max-width:840px · padding 沿用默认 5/6/6</div>
</div>
</div>
</div></div>
</section>
<section class="section" id="a-input">
<h2>输入 · .input / .field / .flabel</h2>
<p class="desc">强描边 + radius-md + focus clay 光环。状态 <code>.is-focused</code> / <code>.is-error</code> / <code>:disabled</code></p>
+10 -17
View File
@@ -125,7 +125,6 @@
.searchbar input::placeholder{ color:var(--fg3); }
/* ---- 统计视图 ---- */
.stats-view{ padding:var(--space-5) var(--space-6) var(--space-6); }
/* 顶行:标题左 + 设备筛选下拉右(对齐线上 stats_page.dart 的 _DeviceDropdown) */
.stats-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-4); }
.stats-head .sh-t{ font-family:var(--font-display); font-weight:700; font-size:20px; color:var(--fg1); }
@@ -155,7 +154,6 @@
.sc-g1{ stop-color:var(--accent); stop-opacity:0; }
/* ---- 账户视图 ---- */
.account-view{ padding:var(--space-5) var(--space-6) var(--space-6); max-width:600px; margin:0 auto; }
.account-view .plan-banner{ margin-bottom:var(--space-5); }
.st-change{ font-family:var(--font-sans); font-size:13px; font-weight:600; color:var(--accent); }
@@ -176,11 +174,9 @@
.dv-menu .menu-item.is-danger:hover{ background:var(--danger-subtle); }
/* ---- 设置视图 ---- */
.settings-view{ padding:var(--space-5) var(--space-6) var(--space-6); max-width:600px; margin:0 auto; }
.side .plan-badge{ width:100%; }
/* ---- 联系视图 ---- */
.contact-view{ padding:var(--space-5) var(--space-6) var(--space-6); max-width:none; }
.contact-intro{ font-size:13px; color:var(--fg2); line-height:var(--leading-relaxed); margin:0 0 var(--space-4); }
.icon-box.is-plain{ background:var(--bg-subtle); color:var(--fg2); }
.hours-card{ display:flex; align-items:center; gap:10px; background:var(--bg-subtle);
@@ -190,7 +186,7 @@
.hours-card .hc-s{ font-family:var(--font-mono); font-size:12px; color:var(--fg3); margin-top:2px; }
/* ---- 购买视图(兑换 & 购买流程,接入主框架内容区) ---- */
.purchase-view{ padding:var(--space-5) var(--space-6) var(--space-8); max-width:840px; margin:0 auto; }
.purchase-view{ padding:var(--space-5) var(--space-6) var(--space-8); }
.pv-back{ display:inline-flex; align-items:center; gap:6px; border:none; background:transparent;
color:var(--accent); font-family:var(--font-sans); font-size:13px; font-weight:600; cursor:pointer;
padding:6px 12px 6px 8px; margin-bottom:var(--space-4); border-radius:var(--radius-full);
@@ -242,7 +238,6 @@
.pv-qr-cap{ font-size:var(--text-sm); font-weight:600; color:var(--fg1); }
/* ---- 我的订单视图(列表 → 详情,接入主框架内容区) ---- */
.orders-view{ padding:var(--space-5) var(--space-6) var(--space-8); max-width:600px; margin:0 auto; }
.od-screen{ display:none; }
.od-screen.is-active{ display:block; }
/* 订单详情 = 一张「收据」卡:顶部渐变条 + 摘要头(金额左/状态右) + 虚线撕裂线 + 明细 */
@@ -346,7 +341,6 @@
background:var(--danger); border:1.5px solid var(--bg); }
/* ---- 通知收件箱视图 ---- */
.notif-view{ padding:var(--space-5) var(--space-6) var(--space-6); max-width:600px; margin:0 auto; }
.notif-row{ display:flex; align-items:flex-start; gap:var(--space-3); padding:14px 16px; border-top:1px solid var(--border); }
.notif-row:first-child{ border-top:none; }
.notif-dot{ flex:none; width:8px; height:8px; margin-top:6px; border-radius:var(--radius-full); background:var(--danger); }
@@ -359,7 +353,6 @@
.notif-s{ font-size:12.5px; color:var(--fg3); margin-top:2px; line-height:var(--leading-normal); }
/* ---- 邀请好友视图 ---- */
.invite-view{ padding:var(--space-5) var(--space-6) var(--space-6); max-width:600px; margin:0 auto; }
.invite-code-card{ margin-bottom:var(--space-5); display:flex; flex-direction:column; gap:var(--space-3); }
.inv-lead{ font-size:13px; color:var(--fg2); line-height:var(--leading-relaxed); }
.inv-code-box, .inv-link-box{ display:flex; align-items:center; gap:var(--space-2);
@@ -531,7 +524,7 @@
<!-- 统计视图 -->
<section class="view" data-view="stats">
<div class="stats-view">
<div class="view-body stats-view">
<div class="stats-head">
<span class="sh-t" data-i18n="nav.stats">统计</span>
<span class="langsel stats-dd" id="devSel">
@@ -583,7 +576,7 @@
<!-- 账户视图 -->
<section class="view" data-view="account">
<div class="account-view">
<div class="view-body account-view">
<div class="plan-banner" id="planBanner">
<div class="pbn-row">
<span class="pbn-badge" id="pbnBadge"><svg id="pbnIcon"><use href="#i-user"/></svg></span>
@@ -656,7 +649,7 @@
<!-- 设备管理视图(自账户页下钻) -->
<section class="view" data-view="devices">
<div class="account-view">
<div class="view-body account-view">
<button class="pv-back" id="dvBack"><svg><use href="#i-arrow-left"/></svg><span data-i18n="dev.back">返回</span></button>
<div class="sec-label"><span data-i18n="acc.devices">设备管理</span> · <span id="devHeaderCount">1 / 5</span></div>
<div class="card card-flush" id="deviceList" style="margin-bottom:var(--space-5)"></div>
@@ -665,7 +658,7 @@
<!-- 设置视图 -->
<section class="view" data-view="settings">
<div class="settings-view">
<div class="view-body settings-view">
<div class="sec-label" data-i18n="set.grpConn">连接</div>
<div class="card card-flush" style="margin-bottom:var(--space-5)">
<div class="setting-row">
@@ -734,7 +727,7 @@
<!-- 联系视图 -->
<section class="view" data-view="contact">
<div class="contact-view">
<div class="view-body">
<p class="contact-intro" data-i18n="contact.intro">遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。</p>
<div class="card card-flush" style="margin-bottom:var(--space-3)">
<div class="setting-row is-tap">
@@ -765,7 +758,7 @@
<!-- 购买视图(兑换 & 购买流程,接入主框架内容区) -->
<section class="view" data-view="purchase">
<div class="purchase-view">
<div class="view-body is-wide purchase-view">
<button class="pv-back" id="pvBack"><svg><use href="#i-arrow-left"/></svg><span data-i18n="pur.back">返回</span></button>
<!-- ① 档位选择:特性统一置顶 + 3 档精简卡 -->
@@ -871,7 +864,7 @@
<!-- 我的订单视图(列表 → 详情,接入主框架内容区) -->
<section class="view" data-view="orders">
<div class="orders-view">
<div class="view-body orders-view">
<button class="pv-back" id="odBack"><svg><use href="#i-arrow-left"/></svg><span data-i18n="ord.back">返回</span></button>
<!-- 列表 -->
@@ -899,7 +892,7 @@
<!-- 通知收件箱视图(占位 · Spec ③) -->
<section class="view" data-view="notifications">
<div class="notif-view">
<div class="view-body notif-view">
<button class="pv-back" id="ntBack"><svg><use href="#i-arrow-left"/></svg><span data-i18n="nt.back">返回</span></button>
<div class="sec-label" data-i18n="nt.title">通知</div>
<div class="card card-flush">
@@ -933,7 +926,7 @@
<!-- 邀请好友视图(占位 · Spec ②) -->
<section class="view" data-view="invite">
<div class="invite-view">
<div class="view-body invite-view">
<button class="pv-back" id="invBack"><svg><use href="#i-arrow-left"/></svg><span data-i18n="inv.back">返回</span></button>
<div class="sec-label" data-i18n="inv.title">邀请好友</div>
<div class="card invite-code-card">