fix(app): 完善英文评分布局与顶部提示
将操作提示移至顶部居中,增加滑入、淡入和轻微缩放动画,保持下方操作可用。 补齐规定赛、同步花样、车轮跳及交互绳等项目英文名称,缩短英文计数按钮文案。 窄横屏下将评分页顶部操作按钮移至第二行,计分表按钮按可用宽度换行;补充翻译、提示和英文布局回归测试。
This commit is contained in:
parent
780411037c
commit
ccdfae4160
@ -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<Offset>(
|
||||
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<double>(
|
||||
begin: .97,
|
||||
end: 1,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: notice == null
|
||||
? const SizedBox.shrink(key: ValueKey('notice-empty'))
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = <Widget>[
|
||||
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,
|
||||
|
||||
@ -27,6 +27,39 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('English translates every built-in competition project', () {
|
||||
const english = Locale('en');
|
||||
const projects = <String, String>{
|
||||
'规定赛(个人参赛)': '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);
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user