完成团队赛、 UI 开发

This commit is contained in:
2026-07-27 18:41:20 +08:00
parent 5ffe524e20
commit b2dd593e93
7 changed files with 277 additions and 119 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -24,7 +24,7 @@ class AppConfig {
static AppPackageInfo? packageInfo;
static const appName = 'SportsX裁判工作台';
static const designSize = Size(375, 812);
static const designSize = Size(640, 1024);
/// 主裁判计分 H5 链接
@@ -10,7 +10,8 @@ import 'package:recording_tool/features/competition_teams/widgets/widget_manual_
import 'package:recording_tool/features/events/model/model_event_info.dart';
import 'package:recording_tool/features/events/request_model/request_model_event.dart';
import 'package:recording_tool/features/events/server/server_events.dart';
import 'package:recording_tool/shared/widgets/app_button.dart';
import 'package:recording_tool/gen/assets.gen.dart';
import 'package:recording_tool/shared/widgets/app_bar.dart';
import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart';
import 'package:recording_tool/shared/widgets/app_webview.dart';
@@ -236,52 +237,68 @@ class _EventTeamMatchPageState extends ConsumerState<EventTeamMatchPage> {
final redTeam = _redTeam;
final blueTeam = _blueTeam;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(),
backgroundColor: const Color(0xFFF6F7F9),
appBar: myAppBar(
context: context,
titleWidget: Text(
'选手检录',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
fontFamily: 'PingFang SC',
),
),
actions: [
TextButton(
key: const ValueKey('event-team-manual-process'),
onPressed: _manualProcess,
style: TextButton.styleFrom(
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 14.w),
minimumSize: Size(72.w, kToolbarHeight),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'人工处理',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.8),
fontFamily: 'PingFang SC',
),
),
),
],
),
body: SafeArea(
top: false,
child: Column(
children: [
SizedBox(height: 10.h),
_MatchMetadata(detail: _detail),
SizedBox(height: 12.h),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 20.h),
padding: EdgeInsets.fromLTRB(8.w, 0, 8.w, 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_MatchMetadata(detail: _detail),
SizedBox(height: 10.h),
SizedBox(height: 8.h),
Align(
alignment: Alignment.centerRight,
child: TextButton(
key: const ValueKey('event-team-manual-process'),
onPressed: _manualProcess,
child: Text(
'人工处理',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF078AF2),
),
),
),
),
_TeamCard(
teamLabel: '',
sideLabel: '',
team: redTeam,
members: _redMembers,
backgroundColor: const Color(0xFFFFEEF0),
backgroundImage: 'assets/images/image_red_team.png',
accentColor: const Color(0xFFE84B5B),
verifiedUserIds: _verifiedUserIds,
winner: _winnerSideIndex == 0,
),
SizedBox(height: 16.h),
SizedBox(height: 8.h),
_TeamCard(
teamLabel: '',
sideLabel: '',
team: blueTeam,
members: _blueMembers,
backgroundColor: const Color(0xFFEDF5FF),
backgroundImage: 'assets/images/image_blue_team.png',
accentColor: const Color(0xFF287FDD),
verifiedUserIds: _verifiedUserIds,
winner: _winnerSideIndex == 1,
@@ -292,21 +309,20 @@ class _EventTeamMatchPageState extends ConsumerState<EventTeamMatchPage> {
),
),
Padding(
padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 20.h),
padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 24.h),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: AppButton(
_TeamActionButton(
label: '继续扫码',
variant: AppButtonVariant.outline,
iconPath: Assets.images.imageScan.path,
onPressed: _continueScan,
),
filled: true,
),
SizedBox(height: 12.h),
SizedBox(
width: double.infinity,
child: AppButton(label: '直接开赛', onPressed: _startDirectly),
_TeamActionButton(
label: '直接开赛',
iconPath: 'assets/images/image_start.png',
onPressed: _startDirectly,
),
],
),
@@ -341,25 +357,30 @@ class _MatchMetadata extends StatelessWidget {
final groupName = detail.groupName?.trim() ?? '';
return Container(
width: double.infinity,
padding: EdgeInsets.all(18.r),
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 12.h),
decoration: BoxDecoration(
color: const Color(0xFFF7F9FC),
borderRadius: BorderRadius.circular(14.r),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 12.r,
offset: Offset(0, 4.h),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: _MetadataText(label: '比赛项目', value: itemName),
),
SizedBox(width: 16.w),
_MetadataText(label: '场地', value: matchPlace),
],
),
SizedBox(height: 14.h),
_MetadataText(label: '组别', value: groupName),
child: Row(
children: [
Expanded(
child: _MetadataText(label: '场地', value: matchPlace),
),
SizedBox(width: 6.w),
Expanded(
child: _MetadataText(label: '赛项', value: itemName),
),
SizedBox(width: 6.w),
Expanded(
child: _MetadataText(label: '组别', value: groupName),
),
],
),
);
@@ -376,13 +397,14 @@ class _MetadataText extends StatelessWidget {
Widget build(BuildContext context) {
return Text(
'$label${value.isEmpty ? '暂无' : value}',
maxLines: 2,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 17.sp,
fontSize: 13.sp,
height: 1.35,
color: const Color(0xFF303640),
color: const Color(0xFF858B95),
fontWeight: FontWeight.w600,
fontFamily: 'PingFang SC',
),
);
}
@@ -390,20 +412,20 @@ class _MetadataText extends StatelessWidget {
class _TeamCard extends StatelessWidget {
const _TeamCard({
required this.teamLabel,
required this.sideLabel,
required this.team,
required this.members,
required this.backgroundColor,
required this.backgroundImage,
required this.accentColor,
required this.verifiedUserIds,
required this.winner,
this.emptyMessage = '暂无成员数据',
});
final String teamLabel;
final String sideLabel;
final CompetitionTeam team;
final List<EventTeamMember> members;
final Color backgroundColor;
final String backgroundImage;
final Color accentColor;
final Set<String> verifiedUserIds;
final bool winner;
@@ -414,30 +436,38 @@ class _TeamCard extends StatelessWidget {
final visibleMembers = members
.where((member) => member.userId.isNotEmpty || member.name.isNotEmpty)
.toList(growable: false);
return AnimatedContainer(
duration: const Duration(milliseconds: 180),
final leader = _leaderOf(visibleMembers);
final players = visibleMembers
.where((member) => !identical(member, leader))
.toList(growable: false);
final leaderVerified =
leader != null && verifiedUserIds.contains(leader.userId);
return Container(
width: double.infinity,
padding: EdgeInsets.all(18.r),
constraints: BoxConstraints(minHeight: 106.h),
padding: EdgeInsets.fromLTRB(12.w, 16.h, 16.w, 12.h),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(14.r),
border: Border.all(
color: winner ? accentColor : accentColor.withValues(alpha: 0.24),
width: winner ? 2 : 1,
image: DecorationImage(
image: AssetImage(backgroundImage),
fit: BoxFit.fill,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
'$teamLabel${team.name}',
'$sideLabel队伍名称${team.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 19.sp,
fontSize: 16.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF252B34),
color: const Color(0xFF30343B),
fontFamily: 'PingFang SC',
),
),
),
@@ -456,78 +486,191 @@ class _TeamCard extends StatelessWidget {
'胜方',
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontSize: 11.sp,
fontWeight: FontWeight.w700,
fontFamily: 'PingFang SC',
),
),
),
],
),
SizedBox(height: 14.h),
SizedBox(height: 6.h),
if (visibleMembers.isEmpty)
Text(
emptyMessage,
style: TextStyle(fontSize: 15.sp, color: const Color(0xFF747D89)),
style: TextStyle(
fontSize: 13.sp,
color: const Color(0xFF747D89),
fontFamily: 'PingFang SC',
),
)
else
...visibleMembers.map(
(member) => _MemberRow(
member: member,
verified: verifiedUserIds.contains(member.userId),
accentColor: accentColor,
else ...[
if (leader != null)
_TeamTextLine(
label: '队长',
text: _formatMember(leader),
showOk: leaderVerified || leader.isLeader,
),
if (players.isNotEmpty)
_TeamTextLine(
label: '选手',
text: players.map(_formatMember).join(''),
showOk: players.any(
(member) => verifiedUserIds.contains(member.userId),
),
),
],
],
),
);
}
EventTeamMember? _leaderOf(List<EventTeamMember> members) {
for (final member in members) {
if (member.isLeader) return member;
}
return members.isEmpty ? null : members.first;
}
String _formatMember(EventTeamMember member) {
final name = member.name.trim().isEmpty ? '暂无' : member.name.trim();
// final id = member.userId.trim();
// return id.isEmpty ? name : '$name$id';
return name;
}
}
class _MemberRow extends StatelessWidget {
const _MemberRow({
required this.member,
required this.verified,
required this.accentColor,
class _TeamTextLine extends StatelessWidget {
const _TeamTextLine({
required this.label,
required this.text,
required this.showOk,
});
final EventTeamMember member;
final bool verified;
final Color accentColor;
final String label;
final String text;
final bool showOk;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 5.h),
padding: EdgeInsets.only(top: 4.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 6.r,
height: 6.r,
decoration: BoxDecoration(
color: accentColor,
shape: BoxShape.circle,
Text(
'$label',
style: TextStyle(
fontSize: 13.sp,
height: 1.35,
color: const Color(0xFF747B86),
fontWeight: FontWeight.w500,
fontFamily: 'PingFang SC',
),
),
SizedBox(width: 10.w),
Expanded(
Flexible(
child: Text(
'${member.isLeader ? '队长' : '选手'}${member.name.isEmpty ? '暂无' : member.name}',
style: TextStyle(fontSize: 16.sp, color: const Color(0xFF303640)),
text.isEmpty ? '暂无' : text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13.sp,
height: 1.35,
color: const Color(0xFF4D545F),
fontWeight: FontWeight.w500,
fontFamily: 'PingFang SC',
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child: verified
? Icon(
Icons.check_circle,
key: ValueKey('verified-member-${member.userId}'),
color: const Color(0xFF18A957),
size: 24.r,
)
: SizedBox(width: 24.r, height: 24.r),
),
if (showOk)
Padding(
padding: EdgeInsets.only(left: 6.w),
child: Image.asset(
Assets.images.imageOk.path,
width: 18.w,
height: 14.h,
fit: BoxFit.contain,
),
),
],
),
);
}
}
class _TeamActionButton extends StatelessWidget {
const _TeamActionButton({
required this.label,
required this.iconPath,
required this.onPressed,
this.filled = false,
});
final String label;
final String iconPath;
final VoidCallback onPressed;
final bool filled;
@override
Widget build(BuildContext context) {
final radius = BorderRadius.circular(28.r);
final content = Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
iconPath,
width: filled ? 18.w : 18.w,
height: filled ? 18.h : 18.h,
fit: BoxFit.contain,
),
SizedBox(width: 4.w),
Text(
label,
style: TextStyle(
color: filled ? Colors.white : const Color(0xFF359FED),
fontSize: 16.sp,
fontWeight: FontWeight.w700,
fontFamily: 'PingFang SC',
),
),
],
);
return SizedBox(
width: double.infinity,
height: 45.h,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: filled
? const LinearGradient(
colors: [Color(0xFF2F8DFF), Color(0xFF57D2EE)],
)
: null,
color: filled ? null : Colors.white,
borderRadius: radius,
border: filled
? null
: Border.all(color: const Color(0xFF9FD0F5), width: 1.w),
boxShadow: filled
? [
BoxShadow(
color: const Color(0xFF2F8DFF).withValues(alpha: 0.24),
blurRadius: 10.r,
offset: Offset(0, 5.h),
),
]
: null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: radius,
child: Center(child: content),
),
),
),
);
}
}
+15
View File
@@ -36,6 +36,10 @@ class $AssetsImagesGen {
AssetGenImage get imageAppNameText =>
const AssetGenImage('assets/images/image_app_name_text.png');
/// File path: assets/images/image_blue_team.png
AssetGenImage get imageBlueTeam =>
const AssetGenImage('assets/images/image_blue_team.png');
/// File path: assets/images/image_dialog_bg.png
AssetGenImage get imageDialogBg =>
const AssetGenImage('assets/images/image_dialog_bg.png');
@@ -52,6 +56,10 @@ class $AssetsImagesGen {
AssetGenImage get imagePageBg =>
const AssetGenImage('assets/images/image_page_bg.png');
/// File path: assets/images/image_red_team.png
AssetGenImage get imageRedTeam =>
const AssetGenImage('assets/images/image_red_team.png');
/// File path: assets/images/image_scan.png
AssetGenImage get imageScan =>
const AssetGenImage('assets/images/image_scan.png');
@@ -60,6 +68,10 @@ class $AssetsImagesGen {
AssetGenImage get imageScanQrcode =>
const AssetGenImage('assets/images/image_scan_qrcode.png');
/// File path: assets/images/image_start.png
AssetGenImage get imageStart =>
const AssetGenImage('assets/images/image_start.png');
/// File path: assets/images/image_vs.png
AssetGenImage get imageVs =>
const AssetGenImage('assets/images/image_vs.png');
@@ -69,12 +81,15 @@ class $AssetsImagesGen {
imageAppBarBg,
imageAppIcon,
imageAppNameText,
imageBlueTeam,
imageDialogBg,
imageLogo,
imageOk,
imagePageBg,
imageRedTeam,
imageScan,
imageScanQrcode,
imageStart,
imageVs,
];
}