From ccdfae4160868df7188599e4213fcd11ebe4cf8d Mon Sep 17 00:00:00 2001 From: ltx <1006158920@qq.com> Date: Wed, 14 Oct 2026 17:23:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(app):=20=E5=AE=8C=E5=96=84=E8=8B=B1?= =?UTF-8?q?=E6=96=87=E8=AF=84=E5=88=86=E5=B8=83=E5=B1=80=E4=B8=8E=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将操作提示移至顶部居中,增加滑入、淡入和轻微缩放动画,保持下方操作可用。 补齐规定赛、同步花样、车轮跳及交互绳等项目英文名称,缩短英文计数按钮文案。 窄横屏下将评分页顶部操作按钮移至第二行,计分表按钮按可用宽度换行;补充翻译、提示和英文布局回归测试。 --- lib/app/localization/app_notice.dart | 18 +- lib/app/localization/app_strings.dart | 21 ++- lib/modules/scoring/views/scoring_page.dart | 13 +- .../scoring/widgets/competition_header.dart | 170 ++++++++++-------- test/app_localization_test.dart | 33 ++++ test/app_notice_test.dart | 26 ++- test/competition_header_test.dart | 34 ++++ 7 files changed, 223 insertions(+), 92 deletions(-) diff --git a/lib/app/localization/app_notice.dart b/lib/app/localization/app_notice.dart index 335d422..5c290c4 100644 --- a/lib/app/localization/app_notice.dart +++ b/lib/app/localization/app_notice.dart @@ -92,21 +92,31 @@ class AppNoticeHost extends StatelessWidget { Positioned( left: 16, right: 16, - bottom: media.viewInsets.bottom + media.padding.bottom + 20, + top: media.padding.top + 28, child: IgnorePointer( child: Center( child: SizedBox( width: width, child: AnimatedSwitcher( - duration: const Duration(milliseconds: 220), + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 180), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeInCubic, transitionBuilder: (child, animation) => SlideTransition( position: Tween( - begin: const Offset(0, .18), + begin: const Offset(0, -.28), end: Offset.zero, ).animate(animation), - child: FadeTransition(opacity: animation, child: child), + child: FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: Tween( + begin: .97, + end: 1, + ).animate(animation), + child: child, + ), + ), ), child: notice == null ? const SizedBox.shrink(key: ValueKey('notice-empty')) diff --git a/lib/app/localization/app_strings.dart b/lib/app/localization/app_strings.dart index 896e452..7db03ae 100644 --- a/lib/app/localization/app_strings.dart +++ b/lib/app/localization/app_strings.dart @@ -145,6 +145,10 @@ abstract final class AppStrings { if (joined.length > 1) { return joined.map((part) => translate(part, locale: locale)).join(' · '); } + final spaced = source.split(' '); + if (spaced.length > 1) { + return spaced.map((part) => translate(part, locale: locale)).join(' '); + } return source; } @@ -293,8 +297,8 @@ abstract final class AppStrings { '当前累计扣分(分)': 'Current total deductions (points)', '本裁判预览;综合系数见成绩汇总': 'Judge preview; combined factor in results', '减数量 / 加数量': 'Decreases / increases', - '减数量': 'Decrease count', - '加数量': 'Increase count', + '减数量': '− Count', + '加数量': '+ Count', '失误项目': 'Error item', '次数': 'Times', '失误次数': 'Error count', @@ -484,8 +488,21 @@ abstract final class AppStrings { '完成质量裁判': 'Execution judge', '主裁判': 'Chief judge', '规定赛': 'Prescribed routine', + '规定赛(个人参赛)': 'Prescribed routine (individual)', + '规定赛(集体参赛)': 'Prescribed routine (team)', + '个人花样': 'Individual freestyle', '个人花样(精英级)': 'Individual freestyle (elite)', + '两人同步花样': 'Two-person synchronized freestyle', + '两人同步花样(精英级)': 'Two-person synchronized freestyle (elite)', + '四人同步花样': 'Four-person synchronized freestyle', + '四人同步花样(精英级)': 'Four-person synchronized freestyle (elite)', + '两人车轮跳花样': 'Two-person wheel rope freestyle', + '两人车轮跳花样(精英级)': 'Two-person wheel rope freestyle (elite)', '车轮跳花样(精英级)': 'Wheel rope freestyle (elite)', + '三人交互绳花样': 'Three-person Double Dutch freestyle', + '三人交互绳花样(精英级)': 'Three-person Double Dutch freestyle (elite)', + '四人交互绳花样': 'Four-person Double Dutch freestyle', + '四人交互绳花样(精英级)': 'Four-person Double Dutch freestyle (elite)', '交互绳花样(精英级)': 'Double Dutch freestyle (elite)', '表演赛(精英级)': 'Show routine (elite)', '个人参赛': 'Individual entry', diff --git a/lib/modules/scoring/views/scoring_page.dart b/lib/modules/scoring/views/scoring_page.dart index 9efdb99..3f1adf7 100644 --- a/lib/modules/scoring/views/scoring_page.dart +++ b/lib/modules/scoring/views/scoring_page.dart @@ -835,8 +835,10 @@ class _DifficultyPanel extends StatelessWidget { ) : _NumberText('${controller.difficultyCounts[levels.single] ?? 0}'), _NumberText(subtotal.toStringAsFixed(2)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + Wrap( + alignment: WrapAlignment.center, + spacing: 6, + runSpacing: 6, children: [ for (final level in levels) _ActionButton( @@ -1284,8 +1286,10 @@ class _PerformancePanel extends StatelessWidget { ), ], ), - Row( - mainAxisAlignment: MainAxisAlignment.center, + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 6, children: [ _DeltaButton( label: '减数量', @@ -1306,7 +1310,6 @@ class _PerformancePanel extends StatelessWidget { } }, ), - const SizedBox(width: 8), _ActionButton( label: '加数量', onTap: controller.isReadOnly diff --git a/lib/modules/scoring/widgets/competition_header.dart b/lib/modules/scoring/widgets/competition_header.dart index 61f7d0b..6b43853 100644 --- a/lib/modules/scoring/widgets/competition_header.dart +++ b/lib/modules/scoring/widgets/competition_header.dart @@ -74,90 +74,88 @@ class CompetitionHeader extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { final compact = constraints.maxWidth < 1180; + final stackActions = constraints.maxWidth < 1100; final titleWidth = (constraints.maxWidth * .19) .clamp(compact ? 170.0 : 220.0, 340.0) .toDouble(); final gap = compact ? 6.0 : 10.0; - return Row( - children: [ - IconButton( - onPressed: onBack, - tooltip: AppStrings.of(context, '返回任务列表'), - icon: const Icon( - Icons.double_arrow_rounded, - color: AppColors.primary, - size: 28, - ), + final leading = [ + IconButton( + onPressed: onBack, + tooltip: AppStrings.of(context, '返回任务列表'), + icon: const Icon( + Icons.double_arrow_rounded, + color: AppColors.primary, + size: 28, ), - SizedBox( - width: titleWidth, - child: Tooltip( - message: eventLabel, - child: AppText( - eventLabel, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: compact ? 16 : 18, - fontWeight: FontWeight.w700, - ), + ), + SizedBox( + width: titleWidth, + child: Tooltip( + message: eventLabel, + child: AppText( + eventLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: compact ? 16 : 18, + fontWeight: FontWeight.w700, ), ), ), - Padding( - padding: EdgeInsets.symmetric(horizontal: gap), - child: const Icon( - Icons.double_arrow_rounded, - color: AppColors.primary, - ), - ), - Expanded( - child: Row( - children: [ - Expanded( - flex: 3, - child: _InfoPill( - icon: Icons.person_outline, - text: competitionLabel, - compact: compact, - ), - ), - SizedBox(width: gap), - Expanded( - flex: 2, - child: _InfoPill( - icon: Icons.layers_outlined, - text: groupLabel, - compact: compact, - ), - ), - SizedBox(width: gap), - Expanded( - flex: 2, - child: _InfoPill( - icon: Icons.location_on_outlined, - text: sessionLabel, - compact: compact, - ), - ), - SizedBox(width: gap), - Expanded( - flex: 2, - child: _InfoPill( - icon: Icons.hub_outlined, - text: profile.roleName, - compact: compact, - ), - ), - ], - ), - ), - const SizedBox(width: 6), - const Icon( - Icons.chevron_right_rounded, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: gap), + child: const Icon( + Icons.double_arrow_rounded, color: AppColors.primary, - size: 30, ), + ), + ]; + final taskInfo = Expanded( + child: Row( + children: [ + Expanded( + flex: 3, + child: _InfoPill( + icon: Icons.person_outline, + text: competitionLabel, + compact: compact, + ), + ), + SizedBox(width: gap), + Expanded( + flex: 2, + child: _InfoPill( + icon: Icons.layers_outlined, + text: groupLabel, + compact: compact, + ), + ), + SizedBox(width: gap), + Expanded( + flex: 2, + child: _InfoPill( + icon: Icons.location_on_outlined, + text: sessionLabel, + compact: compact, + ), + ), + SizedBox(width: gap), + Expanded( + flex: 2, + child: _InfoPill( + icon: Icons.hub_outlined, + text: profile.roleName, + compact: compact, + ), + ), + ], + ), + ); + final actions = Row( + mainAxisSize: MainAxisSize.min, + children: [ Badge( isLabelVisible: correctionTaskCount > 0, backgroundColor: const Color(0xFFE9A400), @@ -206,6 +204,28 @@ class CompetitionHeader extends StatelessWidget { ), ], ); + if (stackActions) { + return Column( + children: [ + Row(children: [...leading, taskInfo]), + const SizedBox(height: 8), + Align(alignment: Alignment.centerRight, child: actions), + ], + ); + } + return Row( + children: [ + ...leading, + taskInfo, + const SizedBox(width: 6), + const Icon( + Icons.chevron_right_rounded, + color: AppColors.primary, + size: 30, + ), + actions, + ], + ); }, ), ), @@ -327,7 +347,7 @@ class _InfoPill extends StatelessWidget { SizedBox(width: compact ? 4 : 7), Flexible( child: Tooltip( - message: text, + message: AppStrings.of(context, text), child: AppText( text, maxLines: 1, diff --git a/test/app_localization_test.dart b/test/app_localization_test.dart index 00e5b66..0af2dca 100644 --- a/test/app_localization_test.dart +++ b/test/app_localization_test.dart @@ -27,6 +27,39 @@ void main() { }, ); + test('English translates every built-in competition project', () { + const english = Locale('en'); + const projects = { + '规定赛(个人参赛)': 'Prescribed routine (individual)', + '规定赛(集体参赛)': 'Prescribed routine (team)', + '个人花样(精英级)': 'Individual freestyle (elite)', + '两人同步花样(精英级)': 'Two-person synchronized freestyle (elite)', + '四人同步花样(精英级)': 'Four-person synchronized freestyle (elite)', + '两人车轮跳花样(精英级)': 'Two-person wheel rope freestyle (elite)', + '三人交互绳花样(精英级)': 'Three-person Double Dutch freestyle (elite)', + '四人交互绳花样(精英级)': 'Four-person Double Dutch freestyle (elite)', + '表演赛(精英级)': 'Show routine (elite)', + }; + for (final project in projects.entries) { + expect(AppStrings.translate(project.key, locale: english), project.value); + } + expect( + AppStrings.translate('四人交互绳花样', locale: english), + 'Four-person Double Dutch freestyle', + ); + expect( + AppStrings.translate('四人交互绳花样(精英级) 1-2 张三', locale: english), + 'Four-person Double Dutch freestyle (elite) 1-2 张三', + ); + expect(AppStrings.translate('四人交互绳花样纪念赛', locale: english), '四人交互绳花样纪念赛'); + }); + + test('English scoring controls use concise labels', () { + const english = Locale('en'); + expect(AppStrings.translate('减数量', locale: english), '− Count'); + expect(AppStrings.translate('加数量', locale: english), '+ Count'); + }); + test('language selection persists and is restored after restart', () async { final directory = await Directory.systemTemp.createTemp('scoring-locale-'); Hive.init(directory.path); diff --git a/test/app_notice_test.dart b/test/app_notice_test.dart index 2ca97a6..5f1953d 100644 --- a/test/app_notice_test.dart +++ b/test/app_notice_test.dart @@ -9,12 +9,17 @@ void main() { var presses = 0; await tester.pumpWidget( MaterialApp( - builder: (context, child) => AppNoticeHost(child: child!), + builder: (context, child) => MediaQuery( + data: MediaQuery.of( + context, + ).copyWith(padding: const EdgeInsets.only(top: 24)), + child: AppNoticeHost(child: child!), + ), home: Scaffold( body: Align( - alignment: Alignment.bottomCenter, + alignment: Alignment.topCenter, child: Padding( - padding: const EdgeInsets.only(bottom: 20), + padding: const EdgeInsets.only(top: 50), child: SizedBox( width: 380, height: 80, @@ -37,10 +42,19 @@ void main() { await tester.tap(find.text('操作按钮')); await tester.pumpAndSettle(); expect(find.text('保存成功'), findsOneWidget); + final cardRect = tester.getRect( + find.byKey(const ValueKey('app-notice-card')), + ); + expect(cardRect.top, greaterThanOrEqualTo(24 + 28)); expect( - tester - .getRect(find.byKey(const ValueKey('app-notice-card'))) - .overlaps(tester.getRect(find.byType(ElevatedButton))), + cardRect.center.dx, + closeTo( + tester.view.physicalSize.width / tester.view.devicePixelRatio / 2, + 1, + ), + ); + expect( + cardRect.overlaps(tester.getRect(find.byType(ElevatedButton))), isTrue, ); diff --git a/test/competition_header_test.dart b/test/competition_header_test.dart index d0d585e..e5a24a3 100644 --- a/test/competition_header_test.dart +++ b/test/competition_header_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:yd_pattern_scoring_system/app/localization/app_language_service.dart'; import 'package:yd_pattern_scoring_system/core/models/judge_account.dart'; import 'package:yd_pattern_scoring_system/modules/scoring/widgets/competition_header.dart'; @@ -18,6 +19,7 @@ void main() { required Size size, required String eventLabel, int correctionTaskCount = 0, + bool correctionMode = false, }) async { await tester.binding.setSurfaceSize(size); addTearDown(() => tester.binding.setSurfaceSize(null)); @@ -36,6 +38,7 @@ void main() { sessionLabel: '决赛场地一第十二场', teamLabel: '跃动科技代表队', correctionTaskCount: correctionTaskCount, + correctionMode: correctionMode, ), ), ), @@ -73,4 +76,35 @@ void main() { expect(find.text('3'), findsOneWidget); expect(tester.takeException(), isNull); }); + + testWidgets('英文评分页顶部在平板宽度下保留全部操作按钮', (tester) async { + AppLanguageService.instance = AppLanguageService.memory(initialCode: 'en'); + addTearDown(() => AppLanguageService.instance = null); + await pumpHeader( + tester, + size: const Size(900, 720), + eventLabel: '2026年全国花样跳绳精英挑战赛暨青少年锦标赛总决赛上海分站', + correctionTaskCount: 3, + ); + + expect(find.text('Correction\ntasks'), findsOneWidget); + expect(find.text('Submit score'), findsOneWidget); + expect(tester.getTopRight(find.text('Submit score')).dx, lessThan(900)); + expect(tester.takeException(), isNull); + }); + + testWidgets('英文修正按钮在单行布局阈值下也不溢出', (tester) async { + AppLanguageService.instance = AppLanguageService.memory(initialCode: 'en'); + addTearDown(() => AppLanguageService.instance = null); + await pumpHeader( + tester, + size: const Size(1100, 720), + eventLabel: '2026年全国花样跳绳精英挑战赛暨青少年锦标赛总决赛上海分站', + correctionMode: true, + ); + + expect(find.text('Submit correction'), findsOneWidget); + expect(tester.getTopRight(find.text('Submit correction')).dx, lessThan(1100)); + expect(tester.takeException(), isNull); + }); }