22e4eb1ead
- 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>
119 lines
4.6 KiB
Dart
119 lines
4.6 KiB
Dart
// stats_page.dart — 统计页(指标卡 + 本周柱状图)
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../pangolin_theme.dart';
|
||
import '../state/app_providers.dart';
|
||
import '../widgets/app_top_bar.dart';
|
||
|
||
class StatsPage extends ConsumerWidget {
|
||
const StatsPage({super.key, required this.isWide});
|
||
final bool isWide;
|
||
|
||
static const _vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0];
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final c = context.pangolin;
|
||
final t = ref.watch(appTextProvider);
|
||
final maxV = _vals.reduce((a, b) => a > b ? a : b);
|
||
// 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'),
|
||
(t.avgPing, '29', 'ms'),
|
||
(t.durMonth, '86.4', 'h'),
|
||
];
|
||
|
||
final body = ListView(
|
||
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 ? 14 : 0),
|
||
child: _MetricCard(label: metrics[i].$1, value: metrics[i].$2, unit: metrics[i].$3),
|
||
),
|
||
),
|
||
]),
|
||
const SizedBox(height: 20),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||
decoration: BoxDecoration(
|
||
color: c.surface,
|
||
borderRadius: BorderRadius.circular(PangolinRadius.lg),
|
||
border: Border.all(color: c.border),
|
||
boxShadow: PangolinShadow.sm,
|
||
),
|
||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Text(t.weekTraffic,
|
||
style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600, fontSize: 13)),
|
||
const SizedBox(height: 18),
|
||
SizedBox(
|
||
// 柱最高 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: 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: 8),
|
||
Text(t.days7[i], style: PangolinText.caption.copyWith(color: c.fg3)),
|
||
]),
|
||
),
|
||
]),
|
||
),
|
||
]),
|
||
),
|
||
],
|
||
);
|
||
|
||
if (isWide) return body;
|
||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
AppTopBar(brand: t.brand),
|
||
PageTitle(title: t.statsTitle),
|
||
Expanded(child: body),
|
||
]);
|
||
}
|
||
}
|
||
|
||
class _MetricCard extends StatelessWidget {
|
||
const _MetricCard({required this.label, required this.value, required this.unit});
|
||
final String label, value, unit;
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final c = context.pangolin;
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
|
||
decoration: BoxDecoration(
|
||
color: c.surface,
|
||
borderRadius: BorderRadius.circular(PangolinRadius.lg),
|
||
border: Border.all(color: c.border),
|
||
boxShadow: PangolinShadow.sm,
|
||
),
|
||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Text(label, overflow: TextOverflow.ellipsis, style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w600)),
|
||
const SizedBox(height: 7),
|
||
Text.rich(TextSpan(
|
||
text: value,
|
||
style: PangolinText.mono.copyWith(fontSize: 24, color: c.fg1, fontWeight: FontWeight.w500),
|
||
children: [TextSpan(text: ' $unit', style: PangolinText.caption.copyWith(color: c.fg3))],
|
||
)),
|
||
]),
|
||
);
|
||
}
|
||
}
|