feat(client): desktop 其他页精还原 + 整页 golden 闸
- stats_page 对照 dapp.jsx DStats 精对齐: padding 32/8、指标卡 padding 16/18· gap 14·值 mono 24、柱状图卡 padding 20/18·标题 13、柱 height 140(容 bar 86)·宽 34 - nodes_page 对照 DServers 微调: padding top 4、网格 gap 10(智能卡置顶保留, 符 §5) - 新增 test/golden/desktop_pages_golden_test.dart: 用 macOS 平台覆盖 + 920×600 surface 让 formFactor=desktop, 截 节点/统计/账户 三页整页 golden 作回归闸(连接页含旋转动画 由组件级 golden 覆盖, 不在此截) 测试: flutter test 79 通过(+3 desktop 整页 golden), analyze 0 error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,8 +52,9 @@ class _NodesPageState extends ConsumerState<NodesPage> {
|
||||
final search = _SearchBox(hint: t.searchPh, onChanged: (v) => setState(() => _q = v));
|
||||
|
||||
if (widget.isWide) {
|
||||
// 对照 dapp.jsx DServers:padding 4/28、网格 gap 10。智能卡置顶(CLAUDE.md §5 规范,三端一致)。
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(28, 6, 28, 24),
|
||||
padding: const EdgeInsets.fromLTRB(28, 4, 28, 24),
|
||||
children: [
|
||||
smartCard,
|
||||
const SizedBox(height: 14),
|
||||
@@ -63,8 +64,8 @@ class _NodesPageState extends ConsumerState<NodesPage> {
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 11,
|
||||
crossAxisSpacing: 11,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 4.4,
|
||||
children: [
|
||||
for (final n in nodes)
|
||||
|
||||
@@ -17,7 +17,8 @@ class StatsPage extends ConsumerWidget {
|
||||
final c = context.pangolin;
|
||||
final t = ref.watch(appTextProvider);
|
||||
final maxV = _vals.reduce((a, b) => a > b ? a : b);
|
||||
final pad = isWide ? 28.0 : 20.0;
|
||||
// desktop 对照 dapp.jsx DStats:padding 32/8、卡 gap 14、柱 height 120·bar×86·宽 34。
|
||||
final pad = isWide ? 32.0 : 20.0;
|
||||
|
||||
final metrics = [
|
||||
(t.trafficMonth, '42.6', 'GB'),
|
||||
@@ -26,20 +27,20 @@ class StatsPage extends ConsumerWidget {
|
||||
];
|
||||
|
||||
final body = ListView(
|
||||
padding: EdgeInsets.fromLTRB(pad, isWide ? 6 : 0, pad, 24),
|
||||
padding: EdgeInsets.fromLTRB(pad, isWide ? 8 : 0, pad, 24),
|
||||
children: [
|
||||
Row(children: [
|
||||
for (var i = 0; i < metrics.length; i++)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: i < metrics.length - 1 ? 12 : 0),
|
||||
padding: EdgeInsets.only(right: i < metrics.length - 1 ? 14 : 0),
|
||||
child: _MetricCard(label: metrics[i].$1, value: metrics[i].$2, unit: metrics[i].$3),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: c.surface,
|
||||
borderRadius: BorderRadius.circular(PangolinRadius.lg),
|
||||
@@ -47,25 +48,29 @@ class StatsPage extends ConsumerWidget {
|
||||
boxShadow: PangolinShadow.sm,
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(t.weekTraffic, style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600)),
|
||||
Text(t.weekTraffic,
|
||||
style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600, fontSize: 13)),
|
||||
const SizedBox(height: 18),
|
||||
SizedBox(
|
||||
height: isWide ? 150 : 120,
|
||||
// 柱最高 86 + 上下数值/星期标签两行 + 间距 ≈ 134,留余量 140 避免溢出。
|
||||
height: 140,
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
for (var i = 0; i < _vals.length; i++)
|
||||
Expanded(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Text('${_vals[i]}', style: PangolinText.mono.copyWith(fontSize: 11, color: c.fg3)),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
height: (isWide ? 104 : 84) * (_vals[i] / maxV),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: c.accent.withOpacity(0.85),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
Text('${_vals[i]}', style: PangolinText.mono.copyWith(fontSize: 10.5, color: c.fg3)),
|
||||
const SizedBox(height: 8),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 34),
|
||||
child: Container(
|
||||
height: 86 * (_vals[i] / maxV),
|
||||
decoration: BoxDecoration(
|
||||
color: c.accent.withOpacity(0.85),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 8),
|
||||
Text(t.days7[i], style: PangolinText.caption.copyWith(color: c.fg3)),
|
||||
]),
|
||||
),
|
||||
@@ -92,7 +97,7 @@ class _MetricCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final c = context.pangolin;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: c.surface,
|
||||
borderRadius: BorderRadius.circular(PangolinRadius.lg),
|
||||
@@ -101,10 +106,10 @@ class _MetricCard extends StatelessWidget {
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, overflow: TextOverflow.ellipsis, style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 7),
|
||||
Text.rich(TextSpan(
|
||||
text: value,
|
||||
style: PangolinText.mono.copyWith(fontSize: 20, color: c.fg1, fontWeight: FontWeight.w500),
|
||||
style: PangolinText.mono.copyWith(fontSize: 24, color: c.fg1, fontWeight: FontWeight.w500),
|
||||
children: [TextSpan(text: ' $unit', style: PangolinText.caption.copyWith(color: c.fg3))],
|
||||
)),
|
||||
]),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// desktop_pages_golden_test.dart — desktop 整页 golden(节点/统计/账户)
|
||||
//
|
||||
// 用 debugDefaultTargetPlatformOverride=macOS + 920×600 surface 让 context.formFactor
|
||||
// 判定为 desktop,渲染 DesktopShell 各页,固化精还原成果作回归闸。
|
||||
// 连接页含连接键无限旋转动画,单独由组件级 golden 覆盖,此处不截。
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pangolin_vpn/bridge/vpn_bridge_mock.dart';
|
||||
import 'package:pangolin_vpn/bridge/vpn_bridge_provider.dart';
|
||||
import 'package:pangolin_vpn/pangolin_theme.dart';
|
||||
import 'package:pangolin_vpn/services/token_store.dart';
|
||||
import 'package:pangolin_vpn/shell/desktop_shell.dart';
|
||||
import 'package:pangolin_vpn/shell/home_shell.dart';
|
||||
import 'package:pangolin_vpn/state/auth_provider.dart';
|
||||
import 'package:pangolin_vpn/state/navigation_provider.dart';
|
||||
|
||||
class _NullTokenStore implements TokenStore {
|
||||
const _NullTokenStore();
|
||||
@override
|
||||
Future<void> saveTokens({required String access, required String refresh}) async {}
|
||||
@override
|
||||
Future<String?> loadAccessToken() async => null;
|
||||
@override
|
||||
Future<String?> loadRefreshToken() async => null;
|
||||
@override
|
||||
Future<void> clear() async {}
|
||||
}
|
||||
|
||||
Future<void> _shoot(WidgetTester tester, NavView view, String name) async {
|
||||
// 平台覆盖须在 test 体内设置并在结束前复位(否则触发 foundation 调试变量未复位断言)。
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
|
||||
tester.view.physicalSize = const Size(920, 600);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
try {
|
||||
await tester.pumpWidget(ProviderScope(
|
||||
overrides: [
|
||||
tokenStoreProvider.overrideWithValue(const _NullTokenStore()),
|
||||
vpnBridgeProvider.overrideWithValue(VpnBridgeMock()),
|
||||
navViewProvider.overrideWith((ref) => view),
|
||||
],
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: PangolinTheme.light,
|
||||
home: const HomeShell(),
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
await expectLater(find.byType(DesktopShell), matchesGoldenFile('goldens/$name.png'));
|
||||
} finally {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('desktop 节点页', (t) => _shoot(t, NavView.servers, 'desktop_servers'));
|
||||
testWidgets('desktop 统计页', (t) => _shoot(t, NavView.stats, 'desktop_stats'));
|
||||
testWidgets('desktop 账户页', (t) => _shoot(t, NavView.account, 'desktop_account'));
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Reference in New Issue
Block a user