From 37f74c14eecd860862303776eafa980e591e8136 Mon Sep 17 00:00:00 2001 From: 7ping Date: Fri, 16 Oct 2026 08:15:31 +0800 Subject: [PATCH 1/8] fix(ui): keep compact section actions on one line on Android --- client/lib/ui/widgets/fx_section_header.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/lib/ui/widgets/fx_section_header.dart b/client/lib/ui/widgets/fx_section_header.dart index 57af2003..1a882b27 100644 --- a/client/lib/ui/widgets/fx_section_header.dart +++ b/client/lib/ui/widgets/fx_section_header.dart @@ -59,7 +59,10 @@ class FxSectionHeader extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { final scale = MediaQuery.textScalerOf(context).scale(1); - final narrow = constraints.maxWidth < 440 || scale >= 1.6; + // 优先单行呈现:普通 Android 窄屏容得下标题和短操作,不应按全局宽度强制换行。 + // 仅在真正紧窄或超大字体时退化为右对齐的第二行。 + final narrow = constraints.maxWidth < 300 || + (scale >= 1.6 && constraints.maxWidth < 440); final action = trailingWidget; if (action == null) { return ConstrainedBox( From e5ebafa1f64aa5f6fd05349cad8e83f8568f5e04 Mon Sep 17 00:00:00 2001 From: 7ping Date: Fri, 16 Oct 2026 08:15:52 +0800 Subject: [PATCH 2/8] fix(android): compact Today dashboard cards and unify question surface --- .../lib/widgets/dashboard/dashboard_body.dart | 86 +++++++++---------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/client/lib/widgets/dashboard/dashboard_body.dart b/client/lib/widgets/dashboard/dashboard_body.dart index 52582cab..c50cd4e5 100644 --- a/client/lib/widgets/dashboard/dashboard_body.dart +++ b/client/lib/widgets/dashboard/dashboard_body.dart @@ -295,7 +295,7 @@ class _DashboardBodyState extends State { final color = _parseColor(item['color']?.toString()); final active = _activeDays(item); return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: color.withValues(alpha: .06), border: Border.all(color: color.withValues(alpha: .18)), @@ -511,7 +511,9 @@ class _DashboardBodyState extends State { children: [ FxSectionHeader( title: '习惯', - trailing: '$checked/${widget.habits.length} 已打卡', + trailing: widget.habits.isEmpty + ? '暂无习惯' + : '$checked/${widget.habits.length} 已打卡', trailingWidget: widget.onViewAllHabits == null ? null @@ -523,7 +525,7 @@ class _DashboardBodyState extends State { ), ), if (visible.isEmpty) - _empty('还没有习惯记录') + _empty('还没有习惯记录', compact: mobile) else ...visible.map((habit) => _habitRow(habit, mobile)), ], @@ -625,6 +627,7 @@ class _DashboardBodyState extends State { Widget _focusCard() { final theme = Theme.of(context); + final mobile = !ResponsiveLayout.isDesktopOrWider(context); final count = (_facts['focus_count'] as num?)?.toInt() ?? 0; final minutes = (_facts['focus_minutes'] as num?)?.toInt() ?? 0; return _card( @@ -659,70 +662,59 @@ class _DashboardBodyState extends State { icon: LucideIcons.timer, variant: FxButtonVariant.outline, size: FxButtonSize.sm, - expanded: true, onPressed: widget.onStartFocus, ), ], ], ), + padding: EdgeInsets.all(mobile ? 12 : 16), ); } Widget _questionCard() { final theme = Theme.of(context); + final mobile = !ResponsiveLayout.isDesktopOrWider(context); final visible = _insights .where((item) => !_ignored.contains(item.id)) .take(1) .toList(growable: false); final insight = visible.isEmpty ? null : visible.first; - return FxInkWell( - borderRadius: BorderRadius.circular(SlowlightRadius.lg), - onTap: insight == null ? null : () => _respond(insight), - child: CustomPaint( - painter: _DashedBorderPainter( - color: fxDivider(context), - radius: SlowlightRadius.lg, - ), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: fxSurface(context), - borderRadius: BorderRadius.circular(SlowlightRadius.lg), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FxChip( - label: '今日提问', - backgroundColor: activePalette.accent.withValues(alpha: .12), - foregroundColor: activePalette.accent, - borderRadius: 999, - padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + return _card( + FxInkWell( + borderRadius: BorderRadius.circular(SlowlightRadius.lg), + onTap: insight == null ? null : () => _respond(insight), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FxChip( + label: '今日提问', + backgroundColor: activePalette.accent.withValues(alpha: .12), + foregroundColor: activePalette.accent, + borderRadius: SlowlightRadius.pill, + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + ), + const SizedBox(height: 8), + Text( + insight?.content ?? '今天暂时没有特别需要关注的变化。', + maxLines: 4, + overflow: TextOverflow.ellipsis, + style: SlowlightTypography.secondary(context).copyWith( + fontWeight: insight == null ? FontWeight.w400 : FontWeight.w600, ), + ), + if (insight != null) ...[ const SizedBox(height: 8), Text( - insight?.content ?? '今天暂时没有特别需要关注的变化。', - maxLines: 4, - overflow: TextOverflow.ellipsis, - style: SlowlightTypography.secondary(context).copyWith( - fontWeight: - insight == null ? FontWeight.w400 : FontWeight.w600, - ), + '点击去回应 →', + style: SlowlightTypography.caption( + context, + ).copyWith(color: theme.colorScheme.onSurfaceVariant), ), - if (insight != null) ...[ - const SizedBox(height: 8), - Text( - '点击去回应 →', - style: SlowlightTypography.caption( - context, - ).copyWith(color: theme.colorScheme.onSurfaceVariant), - ), - ], ], - ), + ], ), ), + padding: EdgeInsets.all(mobile ? 12 : 16), ); } @@ -742,8 +734,8 @@ class _DashboardBodyState extends State { } } - Widget _empty(String text) => Padding( - padding: const EdgeInsets.symmetric(vertical: 14), + Widget _empty(String text, {bool compact = false}) => Padding( + padding: EdgeInsets.symmetric(vertical: compact ? 8 : 14), child: Text( text, style: SlowlightTypography.secondary( From f752faa48e9f682b91045039d140e1f41707d9fe Mon Sep 17 00:00:00 2001 From: 7ping Date: Fri, 16 Oct 2026 08:16:06 +0800 Subject: [PATCH 3/8] refactor(ui): remove obsolete dashboard dashed border painter --- .../lib/widgets/dashboard/dashboard_body.dart | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/client/lib/widgets/dashboard/dashboard_body.dart b/client/lib/widgets/dashboard/dashboard_body.dart index c50cd4e5..1f592477 100644 --- a/client/lib/widgets/dashboard/dashboard_body.dart +++ b/client/lib/widgets/dashboard/dashboard_body.dart @@ -1,5 +1,3 @@ -import 'dart:math' as math; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; @@ -944,34 +942,3 @@ class _SwipeAction extends StatelessWidget { } } -class _DashedBorderPainter extends CustomPainter { - final Color color; - final double radius; - - const _DashedBorderPainter({required this.color, required this.radius}); - - @override - void paint(Canvas canvas, Size size) { - final paint = - Paint() - ..color = color - ..style = PaintingStyle.stroke - ..strokeWidth = 1; - final path = - Path()..addRRect( - RRect.fromRectAndRadius(Offset.zero & size, Radius.circular(radius)), - ); - for (final metric in path.computeMetrics()) { - var distance = 0.0; - while (distance < metric.length) { - final end = math.min(distance + 5, metric.length).toDouble(); - canvas.drawPath(metric.extractPath(distance, end), paint); - distance += 9; - } - } - } - - @override - bool shouldRepaint(covariant _DashedBorderPainter oldDelegate) => - oldDelegate.color != color || oldDelegate.radius != radius; -} From 7ab3de2df516955c5c5e963cc9e9e580d0af08da Mon Sep 17 00:00:00 2001 From: 7ping Date: Fri, 16 Oct 2026 08:16:19 +0800 Subject: [PATCH 4/8] test(ui): ensure Android Today card actions stay inline at normal text scales --- client/test/ui/fx_section_header_test.dart | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/client/test/ui/fx_section_header_test.dart b/client/test/ui/fx_section_header_test.dart index 19699e0e..48324777 100644 --- a/client/test/ui/fx_section_header_test.dart +++ b/client/test/ui/fx_section_header_test.dart @@ -82,6 +82,50 @@ void main() { await disposeFxTestHost(tester); }); + for (final scale in [1.0, 1.3]) { + testWidgets('FxSectionHeader 360dp 常规字体 $scale 时标题操作同一行', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + await tester.pumpWidget( + buildFxTestHost( + theme: AppTheme.lightTheme(), + home: MediaQuery( + data: MediaQueryData( + size: const Size(360, 800), + textScaler: TextScaler.linear(scale), + ), + child: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + // 首页卡片在 360dp 设备上的真实内容宽度。 + width: 304, + child: FxSectionHeader( + title: '今日任务', + trailing: '0/1 完成', + trailingWidget: FxButton( + label: '查看全部', + variant: FxButtonVariant.ghost, + size: FxButtonSize.sm, + onPressed: () {}, + ), + ), + ), + ), + ), + ), + ), + ); + + final titleRect = tester.getRect(find.text('今日任务')); + final actionRect = tester.getRect(find.text('查看全部')); + expect((titleRect.center.dy - actionRect.center.dy).abs(), lessThan(24)); + expect(tester.takeException(), isNull); + await disposeFxTestHost(tester); + }); + } + testWidgets('FxSectionHeader 大字体时将查看全部放到下一行右侧', (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.android; addTearDown(() => debugDefaultTargetPlatformOverride = null); From f8d41b1c150251b8b63b46562195559a739901ae Mon Sep 17 00:00:00 2001 From: 7ping Date: Fri, 16 Oct 2026 08:17:01 +0800 Subject: [PATCH 5/8] feat(ui): reuse FxProgress for Today dimension activity --- .../lib/widgets/dashboard/dashboard_body.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/client/lib/widgets/dashboard/dashboard_body.dart b/client/lib/widgets/dashboard/dashboard_body.dart index 1f592477..e8621d61 100644 --- a/client/lib/widgets/dashboard/dashboard_body.dart +++ b/client/lib/widgets/dashboard/dashboard_body.dart @@ -292,6 +292,7 @@ class _DashboardBodyState extends State { final theme = Theme.of(context); final color = _parseColor(item['color']?.toString()); final active = _activeDays(item); + final activeCount = _lastSevenDays().where((day) => active.contains(_dateKey(day))).length; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( @@ -309,14 +310,24 @@ class _DashboardBodyState extends State { ).copyWith(fontWeight: FontWeight.w600), ), ), - if (compact) + if (compact) ...[ + const SizedBox(width: 8), + Expanded( + child: FxProgress( + value: activeCount / 7, + height: 6, + color: color, + backgroundColor: color.withValues(alpha: .12), + ), + ), + const SizedBox(width: 8), Text( - '${active.length}/7 天', + '$activeCount/7 天', style: SlowlightTypography.caption( context, ).copyWith(color: theme.colorScheme.onSurfaceVariant), - ) - else + ), + ] else ..._lastSevenDays().map( (day) => Container( width: 7, From ca4f82ed5aa92ec3535ac6167da3048b8da44291 Mon Sep 17 00:00:00 2001 From: 7ping Date: Fri, 16 Oct 2026 08:17:08 +0800 Subject: [PATCH 6/8] test(android): preserve floating quick-add task entry --- client/test/screens/home_screen_test.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/test/screens/home_screen_test.dart b/client/test/screens/home_screen_test.dart index cce7c0eb..b1910094 100644 --- a/client/test/screens/home_screen_test.dart +++ b/client/test/screens/home_screen_test.dart @@ -36,6 +36,14 @@ void main() { }); } + testWidgets('移动端保留可拖动的悬浮快速添加任务入口', (tester) async { + await pumpAt(tester, size: const Size(360, 800)); + expect(fxTooltipFinder('快速记录任务(按住可拖动)'), findsOneWidget); + expect(fxTooltipFinder('记录任务'), findsOneWidget); + expect(tester.takeException(), isNull); + await disposeFxTestHost(tester); + }); + testWidgets('360×800 在 130% 系统字体下不产生布局异常', (tester) async { await pumpAt( tester, From ac54da6a7db33dfc2fa6ec446c13c6cd09282f5e Mon Sep 17 00:00:00 2001 From: 7ping Date: Sun, 18 Oct 2026 21:33:07 +0800 Subject: [PATCH 7/8] test(android): apply real narrow width to FxSectionHeader large text assertion --- client/test/ui/fx_section_header_test.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/test/ui/fx_section_header_test.dart b/client/test/ui/fx_section_header_test.dart index 48324777..145e1b00 100644 --- a/client/test/ui/fx_section_header_test.dart +++ b/client/test/ui/fx_section_header_test.dart @@ -139,16 +139,22 @@ void main() { textScaler: TextScaler.linear(2), ), child: Scaffold( - body: Padding( - padding: const EdgeInsets.all(16), - child: FxSectionHeader( - title: '今日任务', + body: Align( + alignment: Alignment.topLeft, + child: SizedBox( + // 测试真实窄屏卡片约束,避免仅改变 MediaQuery.size。 + width: 328, + child: Padding( + padding: const EdgeInsets.all(16), + child: FxSectionHeader( + title: '今日任务', trailing: '3/5 已完成', trailingWidget: FxButton( label: '查看全部', variant: FxButtonVariant.ghost, size: FxButtonSize.sm, onPressed: () {}, + ), ), ), ), From 19eb6630b9258999f2de3a825b3dad48df45c9f9 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sun, 18 Oct 2026 21:33:44 +0800 Subject: [PATCH 8/8] fix(test): balance nested narrow-layout widget test --- client/test/ui/fx_section_header_test.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client/test/ui/fx_section_header_test.dart b/client/test/ui/fx_section_header_test.dart index 145e1b00..faaeb3ef 100644 --- a/client/test/ui/fx_section_header_test.dart +++ b/client/test/ui/fx_section_header_test.dart @@ -142,18 +142,19 @@ void main() { body: Align( alignment: Alignment.topLeft, child: SizedBox( - // 测试真实窄屏卡片约束,避免仅改变 MediaQuery.size。 + // MediaQuery.size 不会约束测试视口;模拟实际 360dp 的卡片宽度。 width: 328, child: Padding( padding: const EdgeInsets.all(16), child: FxSectionHeader( title: '今日任务', - trailing: '3/5 已完成', - trailingWidget: FxButton( - label: '查看全部', - variant: FxButtonVariant.ghost, - size: FxButtonSize.sm, - onPressed: () {}, + trailing: '3/5 已完成', + trailingWidget: FxButton( + label: '查看全部', + variant: FxButtonVariant.ghost, + size: FxButtonSize.sm, + onPressed: () {}, + ), ), ), ),