2 Commits
Author SHA1 Message Date
linfeng b2dd593e93 完成团队赛、 UI 开发 2026-07-27 18:41:20 +08:00
linfeng 5ffe524e20 完成选手信息页 2026-07-27 17:57:09 +08:00
13 changed files with 703 additions and 320 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

+1 -1
View File
@@ -24,7 +24,7 @@ class AppConfig {
static AppPackageInfo? packageInfo; static AppPackageInfo? packageInfo;
static const appName = 'SportsX裁判工作台'; static const appName = 'SportsX裁判工作台';
static const designSize = Size(375, 812); static const designSize = Size(640, 1024);
/// 主裁判计分 H5 链接 /// 主裁判计分 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/model/model_event_info.dart';
import 'package:recording_tool/features/events/request_model/request_model_event.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/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_qr_scanner_dialog.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart';
import 'package:recording_tool/shared/widgets/app_webview.dart'; import 'package:recording_tool/shared/widgets/app_webview.dart';
@@ -236,52 +237,68 @@ class _EventTeamMatchPageState extends ConsumerState<EventTeamMatchPage> {
final redTeam = _redTeam; final redTeam = _redTeam;
final blueTeam = _blueTeam; final blueTeam = _blueTeam;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: const Color(0xFFF6F7F9),
appBar: AppBar(), 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( body: SafeArea(
top: false, top: false,
child: Column( child: Column(
children: [ children: [
SizedBox(height: 10.h),
_MatchMetadata(detail: _detail),
SizedBox(height: 12.h),
Expanded( Expanded(
child: SingleChildScrollView( 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( _TeamCard(
teamLabel: '', sideLabel: '',
team: redTeam, team: redTeam,
members: _redMembers, members: _redMembers,
backgroundColor: const Color(0xFFFFEEF0), backgroundImage: 'assets/images/image_red_team.png',
accentColor: const Color(0xFFE84B5B), accentColor: const Color(0xFFE84B5B),
verifiedUserIds: _verifiedUserIds, verifiedUserIds: _verifiedUserIds,
winner: _winnerSideIndex == 0, winner: _winnerSideIndex == 0,
), ),
SizedBox(height: 16.h), SizedBox(height: 8.h),
_TeamCard( _TeamCard(
teamLabel: '', sideLabel: '',
team: blueTeam, team: blueTeam,
members: _blueMembers, members: _blueMembers,
backgroundColor: const Color(0xFFEDF5FF), backgroundImage: 'assets/images/image_blue_team.png',
accentColor: const Color(0xFF287FDD), accentColor: const Color(0xFF287FDD),
verifiedUserIds: _verifiedUserIds, verifiedUserIds: _verifiedUserIds,
winner: _winnerSideIndex == 1, winner: _winnerSideIndex == 1,
@@ -292,21 +309,20 @@ class _EventTeamMatchPageState extends ConsumerState<EventTeamMatchPage> {
), ),
), ),
Padding( 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( child: Column(
children: [ children: [
SizedBox( _TeamActionButton(
width: double.infinity, label: '继续扫码',
child: AppButton( iconPath: Assets.images.imageScan.path,
label: '继续扫码', onPressed: _continueScan,
variant: AppButtonVariant.outline, filled: true,
onPressed: _continueScan,
),
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
SizedBox( _TeamActionButton(
width: double.infinity, label: '直接开赛',
child: AppButton(label: '直接开赛', onPressed: _startDirectly), iconPath: 'assets/images/image_start.png',
onPressed: _startDirectly,
), ),
], ],
), ),
@@ -341,25 +357,30 @@ class _MatchMetadata extends StatelessWidget {
final groupName = detail.groupName?.trim() ?? ''; final groupName = detail.groupName?.trim() ?? '';
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.all(18.r), padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 12.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF7F9FC), color: Colors.white,
borderRadius: BorderRadius.circular(14.r), boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 12.r,
offset: Offset(0, 4.h),
),
],
), ),
child: Column( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Expanded(
children: [ child: _MetadataText(label: '场地', value: matchPlace),
Expanded( ),
child: _MetadataText(label: '比赛项目', value: itemName), SizedBox(width: 6.w),
), Expanded(
SizedBox(width: 16.w), child: _MetadataText(label: '赛项', value: itemName),
_MetadataText(label: '场地', value: matchPlace), ),
], SizedBox(width: 6.w),
Expanded(
child: _MetadataText(label: '组别', value: groupName),
), ),
SizedBox(height: 14.h),
_MetadataText(label: '组别', value: groupName),
], ],
), ),
); );
@@ -376,13 +397,14 @@ class _MetadataText extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Text( return Text(
'$label${value.isEmpty ? '暂无' : value}', '$label${value.isEmpty ? '暂无' : value}',
maxLines: 2, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 17.sp, fontSize: 13.sp,
height: 1.35, height: 1.35,
color: const Color(0xFF303640), color: const Color(0xFF858B95),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontFamily: 'PingFang SC',
), ),
); );
} }
@@ -390,20 +412,20 @@ class _MetadataText extends StatelessWidget {
class _TeamCard extends StatelessWidget { class _TeamCard extends StatelessWidget {
const _TeamCard({ const _TeamCard({
required this.teamLabel, required this.sideLabel,
required this.team, required this.team,
required this.members, required this.members,
required this.backgroundColor, required this.backgroundImage,
required this.accentColor, required this.accentColor,
required this.verifiedUserIds, required this.verifiedUserIds,
required this.winner, required this.winner,
this.emptyMessage = '暂无成员数据', this.emptyMessage = '暂无成员数据',
}); });
final String teamLabel; final String sideLabel;
final CompetitionTeam team; final CompetitionTeam team;
final List<EventTeamMember> members; final List<EventTeamMember> members;
final Color backgroundColor; final String backgroundImage;
final Color accentColor; final Color accentColor;
final Set<String> verifiedUserIds; final Set<String> verifiedUserIds;
final bool winner; final bool winner;
@@ -414,30 +436,38 @@ class _TeamCard extends StatelessWidget {
final visibleMembers = members final visibleMembers = members
.where((member) => member.userId.isNotEmpty || member.name.isNotEmpty) .where((member) => member.userId.isNotEmpty || member.name.isNotEmpty)
.toList(growable: false); .toList(growable: false);
return AnimatedContainer( final leader = _leaderOf(visibleMembers);
duration: const Duration(milliseconds: 180), final players = visibleMembers
.where((member) => !identical(member, leader))
.toList(growable: false);
final leaderVerified =
leader != null && verifiedUserIds.contains(leader.userId);
return Container(
width: double.infinity, 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( decoration: BoxDecoration(
color: backgroundColor, image: DecorationImage(
borderRadius: BorderRadius.circular(14.r), image: AssetImage(backgroundImage),
border: Border.all( fit: BoxFit.fill,
color: winner ? accentColor : accentColor.withValues(alpha: 0.24),
width: winner ? 2 : 1,
), ),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
'$teamLabel${team.name}', '$sideLabel队伍名称${team.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 19.sp, fontSize: 16.sp,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: const Color(0xFF252B34), color: const Color(0xFF30343B),
fontFamily: 'PingFang SC',
), ),
), ),
), ),
@@ -456,25 +486,110 @@ class _TeamCard extends StatelessWidget {
'胜方', '胜方',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 12.sp, fontSize: 11.sp,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontFamily: 'PingFang SC',
), ),
), ),
), ),
], ],
), ),
SizedBox(height: 14.h), SizedBox(height: 6.h),
if (visibleMembers.isEmpty) if (visibleMembers.isEmpty)
Text( Text(
emptyMessage, emptyMessage,
style: TextStyle(fontSize: 15.sp, color: const Color(0xFF747D89)), style: TextStyle(
fontSize: 13.sp,
color: const Color(0xFF747D89),
fontFamily: 'PingFang SC',
),
) )
else else ...[
...visibleMembers.map( if (leader != null)
(member) => _MemberRow( _TeamTextLine(
member: member, label: '队长',
verified: verifiedUserIds.contains(member.userId), text: _formatMember(leader),
accentColor: accentColor, 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 _TeamTextLine extends StatelessWidget {
const _TeamTextLine({
required this.label,
required this.text,
required this.showOk,
});
final String label;
final String text;
final bool showOk;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 4.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'$label',
style: TextStyle(
fontSize: 13.sp,
height: 1.35,
color: const Color(0xFF747B86),
fontWeight: FontWeight.w500,
fontFamily: 'PingFang SC',
),
),
Flexible(
child: Text(
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',
),
),
),
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,
), ),
), ),
], ],
@@ -483,50 +598,78 @@ class _TeamCard extends StatelessWidget {
} }
} }
class _MemberRow extends StatelessWidget { class _TeamActionButton extends StatelessWidget {
const _MemberRow({ const _TeamActionButton({
required this.member, required this.label,
required this.verified, required this.iconPath,
required this.accentColor, required this.onPressed,
this.filled = false,
}); });
final EventTeamMember member; final String label;
final bool verified; final String iconPath;
final Color accentColor; final VoidCallback onPressed;
final bool filled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( final radius = BorderRadius.circular(28.r);
padding: EdgeInsets.symmetric(vertical: 5.h), final content = Row(
child: Row( mainAxisAlignment: MainAxisAlignment.center,
children: [ mainAxisSize: MainAxisSize.min,
Container( children: [
width: 6.r, Image.asset(
height: 6.r, iconPath,
decoration: BoxDecoration( width: filled ? 18.w : 18.w,
color: accentColor, height: filled ? 18.h : 18.h,
shape: BoxShape.circle, 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',
), ),
SizedBox(width: 10.w), ),
Expanded( ],
child: Text( );
'${member.isLeader ? '队长' : '选手'}${member.name.isEmpty ? '暂无' : member.name}',
style: TextStyle(fontSize: 16.sp, color: const Color(0xFF303640)), 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),
), ),
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),
),
],
), ),
); );
} }
+339 -176
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/app/config/app_config.dart'; import 'package:recording_tool/app/config/app_config.dart';
@@ -67,42 +68,41 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
final state = ref.watch(eventInfoProvider); final state = ref.watch(eventInfoProvider);
final profile = state.profile; final profile = state.profile;
return Scaffold( return AnnotatedRegion<SystemUiOverlayStyle>(
backgroundColor: Colors.white, value: SystemUiOverlayStyle.light.copyWith(
body: SafeArea( statusBarColor: Colors.transparent,
child: Column( systemNavigationBarColor: const Color(0xFFF5F6F8),
crossAxisAlignment: CrossAxisAlignment.start, ),
child: Scaffold(
backgroundColor: const Color(0xFFF5F6F8),
appBar: myAppBar(context: context),
body: Stack(
children: [ children: [
_Header(onBack: () => AppNavigator.pop(context: context)), const _TopGradientHeader(),
Expanded( SafeArea(
child: SingleChildScrollView( bottom: false,
padding: EdgeInsets.fromLTRB(62.w, 8.h, 62.w, 40.h), child: Column(
child: Column( children: [
children: [ // _Header(onBack: () => AppNavigator.pop(context: context)),
_ProfileSection(profile: profile), SizedBox(height: 10.h),
SizedBox(height: 20.h), Padding(
Text( padding: EdgeInsets.symmetric(horizontal: 10.w),
state.eventTitle, child: _ProfileSection(
style: TextStyle( profile: profile,
fontSize: 20.sp, eventTitle: state.eventTitle,
height: 1.2,
color: const Color(0xFF2F2F2F),
fontWeight: FontWeight.w500,
),
), ),
SizedBox(height: 24.h), ),
SizedBox( SizedBox(height: 8.h),
height: 400.h, Expanded(
child: _ScheduleList( child: _ScheduleList(
state: state, state: state,
onRetry: () => ref onRetry: () => ref
.read(eventInfoProvider.notifier) .read(eventInfoProvider.notifier)
.loadRegistrationList(), .loadRegistrationList(),
onItemTap: onItemTap, onItemTap: onItemTap,
),
), ),
], ),
), ],
), ),
), ),
], ],
@@ -112,6 +112,31 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
} }
} }
class _TopGradientHeader extends StatelessWidget {
const _TopGradientHeader();
@override
Widget build(BuildContext context) {
return SizedBox(
height: 219.h,
width: double.infinity,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(36.r),
bottomRight: Radius.circular(36.r),
),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF5EC9FF), Color(0xFF178BFF)],
),
),
),
);
}
}
Widget buildEventRegistrationDestination({ Widget buildEventRegistrationDestination({
required EventRegistrationItem item, required EventRegistrationItem item,
required String playerId, required String playerId,
@@ -135,16 +160,16 @@ class _Header extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: 52.h, height: 44.h,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: IconButton( child: IconButton(
onPressed: onBack, onPressed: onBack,
icon: Icon(Icons.arrow_back_ios_new, size: 32.r), icon: Icon(Icons.chevron_left_rounded, size: 28.r),
color: Colors.black, color: Colors.white,
tooltip: '返回', tooltip: '返回',
padding: EdgeInsets.only(left: 16.w), padding: EdgeInsets.only(left: 10.w),
constraints: BoxConstraints(minWidth: 56.w, minHeight: 52.h), constraints: BoxConstraints(minWidth: 44.w, minHeight: 44.h),
), ),
), ),
); );
@@ -152,52 +177,143 @@ class _Header extends StatelessWidget {
} }
class _ProfileSection extends StatelessWidget { class _ProfileSection extends StatelessWidget {
const _ProfileSection({required this.profile}); const _ProfileSection({required this.profile, required this.eventTitle});
final EventProfile profile; final EventProfile profile;
final String eventTitle;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( final title = eventTitle.trim().isEmpty ? '赛事信息' : eventTitle.trim();
crossAxisAlignment: CrossAxisAlignment.start, return Container(
children: [ clipBehavior: Clip.antiAlias,
profile.avatarUrl.isEmpty padding: EdgeInsets.fromLTRB(12.w, 10.h, 12.w, 12.h),
? AppAvatar(size: 50.r) decoration: BoxDecoration(
: AppAvatar(size: 50.r, imageUrl: profile.avatarUrl), color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
SizedBox(width: 28.w), ),
Expanded( child: Stack(
child: Padding( children: [
padding: EdgeInsets.only(top: 30.h), // Positioned(
child: Column( // top: -28.h,
crossAxisAlignment: CrossAxisAlignment.start, // right: -14.w,
children: [ // child: Transform.rotate(
Text( // angle: 0.68,
profile.name, // child: Container(
maxLines: 1, // width: 110.w,
overflow: TextOverflow.ellipsis, // height: 138.h,
style: TextStyle( // decoration: BoxDecoration(
fontSize: 22.sp, // gradient: LinearGradient(
height: 1.2, // begin: Alignment.topCenter,
color: const Color(0xFF2F2F2F), // end: Alignment.bottomCenter,
// colors: [
// const Color(0xFF53D7FF).withValues(alpha: 0.12),
// const Color(0xFFFF85C8).withValues(alpha: 0.13),
// ],
// ),
// ),
// ),
// ),
// ),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
profile.avatarUrl.isEmpty
? AppAvatar(
size: 52.r,
initials: profile.name.isEmpty ? 'S' : profile.name,
)
: AppAvatar(size: 52.r, imageUrl: profile.avatarUrl),
SizedBox(width: 12.w),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.name.isEmpty ? '参赛选手' : profile.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18.sp,
height: 1.2,
color: const Color(0xFF33363B),
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 5.h),
Text(
profile.phone,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10.sp,
height: 1.2,
color: const Color(0xFF969CA6),
),
),
],
),
), ),
), ],
SizedBox(height: 36.h), ),
Text( SizedBox(height: 10.h),
profile.phone, Divider(
maxLines: 1, height: 1.h,
overflow: TextOverflow.ellipsis, thickness: 1.h,
style: TextStyle( color: const Color(0xFFE8EAED),
fontSize: 22.sp, ),
height: 1.2, SizedBox(height: 10.h),
color: const Color(0xFF2F2F2F), _EventTitleHighlight(title: title),
), ],
),
],
),
), ),
],
),
);
}
}
class _EventTitleHighlight extends StatelessWidget {
const _EventTitleHighlight({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: IntrinsicWidth(
child: Stack(
alignment: Alignment.bottomLeft,
children: [
Positioned(
left: 0,
right: 0,
bottom: 2.h,
child: Container(
height: 8.h,
decoration: BoxDecoration(
color: const Color(0xFFB8F5E8),
borderRadius: BorderRadius.circular(4.r),
),
),
),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16.sp,
height: 1.25,
color: const Color(0xFF33363B),
fontWeight: FontWeight.w600,
),
),
],
), ),
], ),
); );
} }
} }
@@ -223,97 +339,68 @@ class _ScheduleList extends StatelessWidget {
Text( Text(
'暂无赛事报名信息', '暂无赛事报名信息',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 18.sp, color: const Color(0xFF2F2F2F)), style: TextStyle(fontSize: 16.sp, color: const Color(0xFF3A3D42)),
), ),
SizedBox(height: 18.h), SizedBox(height: 14.h),
TextButton(onPressed: onRetry, child: const Text('重新加载')), TextButton(onPressed: onRetry, child: const Text('重新加载')),
], ],
), ),
); );
} }
return ListView.builder( return ListView.separated(
padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 24.h),
itemCount: state.items.length, itemCount: state.items.length,
separatorBuilder: (_, _) => SizedBox(height: 8.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = state.items[index]; final item = state.items[index];
return _ScheduleCard(item: item, onTap: () => onItemTap(item)); return _ScheduleCard(
item: item,
scheduleIndex: index + 1,
onTap: () => onItemTap(item),
);
}, },
); );
} }
} }
class _ScheduleCard extends StatelessWidget { class _ScheduleCard extends StatelessWidget {
const _ScheduleCard({required this.item, required this.onTap}); const _ScheduleCard({
required this.item,
required this.scheduleIndex,
required this.onTap,
});
final EventRegistrationItem item; final EventRegistrationItem item;
final int scheduleIndex;
final VoidCallback onTap; final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(6.r),
child: Container( child: Container(
constraints: BoxConstraints(minHeight: 138.h), constraints: BoxConstraints(minHeight: 86.h),
padding: EdgeInsets.fromLTRB(10.w, 14.h, 14.w, 0), padding: EdgeInsets.fromLTRB(12.w, 10.h, 14.w, 10.h),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF7A7A7A)), color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Column( child: _ScheduleDetails(
crossAxisAlignment: CrossAxisAlignment.start, item: item,
children: [ scheduleIndex: scheduleIndex,
Text(
item.groupName.isEmpty
? item.itemName
: '${item.itemName} ${item.groupName}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.sp,
height: 1.2,
color: const Color(0xFF2F2F2F),
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 26.h),
Text(
item.matchPlace,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.sp,
height: 1.2,
color: const Color(0xFF2F2F2F),
),
),
SizedBox(height: 26.h),
Text(
item.scheduleTime,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.sp,
height: 1.2,
color: const Color(0xFF2F2F2F),
),
),
],
), ),
), ),
SizedBox(width: 12.w), SizedBox(width: 12.w),
SizedBox( _ScheduleStatus(item: item),
width: 148.w,
height: 120.h,
child: Align(
alignment: Alignment.center,
child: _ScheduleBadge(item: item),
),
),
], ],
), ),
), ),
@@ -322,72 +409,148 @@ class _ScheduleCard extends StatelessWidget {
} }
} }
class _ScheduleBadge extends StatelessWidget { class _ScheduleDetails extends StatelessWidget {
const _ScheduleBadge({required this.item}); const _ScheduleDetails({required this.item, required this.scheduleIndex});
final EventRegistrationItem item;
final int scheduleIndex;
@override
Widget build(BuildContext context) {
final memberLine = _formatMemberLine(item);
final opponentLine = _formatOpponentLine(item);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_formatTitle(item),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13.sp,
height: 1.2,
color: const Color(0xFF30343A),
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 6.h),
_MetaText(_formatVenue(item, scheduleIndex)),
SizedBox(height: 3.h),
_MetaText(_formatScheduleTime(item)),
if (memberLine.isNotEmpty) ...[
SizedBox(height: 3.h),
_MetaText(memberLine),
],
if (opponentLine.isNotEmpty) ...[
SizedBox(height: 3.h),
_MetaText(opponentLine),
],
],
);
}
}
class _MetaText extends StatelessWidget {
const _MetaText(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10.sp,
height: 1.2,
color: const Color(0xFF6E747D),
fontWeight: FontWeight.w400,
),
);
}
}
class _ScheduleStatus extends StatelessWidget {
const _ScheduleStatus({required this.item});
final EventRegistrationItem item; final EventRegistrationItem item;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (item.statusLabel != null) { final status = item.statusLabel;
return CustomPaint( if (status != null) {
painter: _CutCornerBorderPainter(color: const Color(0xFF7A7A7A)), return Text(
child: SizedBox( status,
width: 148.w, style: TextStyle(
height: 90.h, fontSize: 10.sp,
child: Center( height: 1.2,
child: Text( color: const Color(0xFF1DBF73),
item.statusLabel!, fontWeight: FontWeight.w500,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 20.sp, color: const Color(0xFF2F2F2F)),
),
),
), ),
); );
} }
return Text( return Text(
item.matchPlace, _formatNumberBadge(item),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 34.sp, fontSize: 20.sp,
height: 1, height: 1,
color: const Color(0xFF2F2F2F), color: const Color(0xFFF27D69),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
); );
} }
} }
class _CutCornerBorderPainter extends CustomPainter { String _formatTitle(EventRegistrationItem item) {
const _CutCornerBorderPainter({required this.color}); if (item.groupName.isEmpty) return item.itemName;
return '${item.itemName} ${item.groupName}';
}
final Color color; String _formatVenue(EventRegistrationItem item, int scheduleIndex) {
final place = item.matchPlace.trim();
if (place.isEmpty) return '$scheduleIndex号场馆$scheduleIndex区';
return '$place号场馆$scheduleIndex区';
}
@override String _formatScheduleTime(EventRegistrationItem item) {
void paint(Canvas canvas, Size size) { final start = DateTime.tryParse(item.matchStartTime);
final side = 12.r; final end = DateTime.tryParse(item.matchEndTime);
final path = Path() if (start == null && end == null) return item.scheduleTime;
..moveTo(side, 0) if (start != null && end != null) {
..lineTo(size.width - side, 0) return '${start.month}${start.day}${_formatClock(start)}-${_formatClock(end)}';
..lineTo(size.width, side)
..lineTo(size.width, size.height - side)
..lineTo(size.width - side, size.height)
..lineTo(side, size.height)
..lineTo(0, size.height - side)
..lineTo(0, side)
..close();
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = color;
canvas.drawPath(path, paint);
} }
final value = start ?? end!;
return '${value.month}${value.day}${_formatClock(value)}';
}
@override String _formatClock(DateTime value) {
bool shouldRepaint(covariant _CutCornerBorderPainter oldDelegate) { return '${value.hour}:${value.minute.toString().padLeft(2, '0')}';
return oldDelegate.color != color; }
}
String _formatMemberLine(EventRegistrationItem item) {
final names = item.teamMembers
.map((member) => member.name.trim())
.where((name) => name.isNotEmpty)
.toList(growable: false);
return names.join('');
}
String _formatOpponentLine(EventRegistrationItem item) {
final opponent = item.opponentName.trim();
if (opponent.isEmpty) return '';
return '对手:$opponent';
}
String _formatNumberBadge(EventRegistrationItem item) {
final place = item.matchPlace.trim();
if (place.isEmpty) return '';
final match = RegExp(r'\d+').firstMatch(place);
if (match != null) return '${match.group(0)}';
return place;
} }
+30
View File
@@ -24,6 +24,10 @@ class $AssetsHtmlGen {
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
/// File path: assets/images/image_app_bar_bg.png
AssetGenImage get imageAppBarBg =>
const AssetGenImage('assets/images/image_app_bar_bg.png');
/// File path: assets/images/image_app_icon.png /// File path: assets/images/image_app_icon.png
AssetGenImage get imageAppIcon => AssetGenImage get imageAppIcon =>
const AssetGenImage('assets/images/image_app_icon.png'); const AssetGenImage('assets/images/image_app_icon.png');
@@ -32,6 +36,10 @@ class $AssetsImagesGen {
AssetGenImage get imageAppNameText => AssetGenImage get imageAppNameText =>
const AssetGenImage('assets/images/image_app_name_text.png'); 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 /// File path: assets/images/image_dialog_bg.png
AssetGenImage get imageDialogBg => AssetGenImage get imageDialogBg =>
const AssetGenImage('assets/images/image_dialog_bg.png'); const AssetGenImage('assets/images/image_dialog_bg.png');
@@ -40,26 +48,48 @@ class $AssetsImagesGen {
AssetGenImage get imageLogo => AssetGenImage get imageLogo =>
const AssetGenImage('assets/images/image_logo.png'); const AssetGenImage('assets/images/image_logo.png');
/// File path: assets/images/image_ok.png
AssetGenImage get imageOk =>
const AssetGenImage('assets/images/image_ok.png');
/// File path: assets/images/image_page_bg.png /// File path: assets/images/image_page_bg.png
AssetGenImage get imagePageBg => AssetGenImage get imagePageBg =>
const AssetGenImage('assets/images/image_page_bg.png'); 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');
/// File path: assets/images/image_scan_qrcode.png /// File path: assets/images/image_scan_qrcode.png
AssetGenImage get imageScanQrcode => AssetGenImage get imageScanQrcode =>
const AssetGenImage('assets/images/image_scan_qrcode.png'); 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 /// File path: assets/images/image_vs.png
AssetGenImage get imageVs => AssetGenImage get imageVs =>
const AssetGenImage('assets/images/image_vs.png'); const AssetGenImage('assets/images/image_vs.png');
/// List of all assets /// List of all assets
List<AssetGenImage> get values => [ List<AssetGenImage> get values => [
imageAppBarBg,
imageAppIcon, imageAppIcon,
imageAppNameText, imageAppNameText,
imageBlueTeam,
imageDialogBg, imageDialogBg,
imageLogo, imageLogo,
imageOk,
imagePageBg, imagePageBg,
imageRedTeam,
imageScan,
imageScanQrcode, imageScanQrcode,
imageStart,
imageVs, imageVs,
]; ];
} }
+71 -25
View File
@@ -1,31 +1,77 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/app/router/app_navigator.dart';
import 'package:recording_tool/gen/assets.gen.dart';
AppBar myAppBar({ /// 全局页面标题栏:背景图撑满 + 居中标题 + 返回按钮。
/// 可直接赋给 [Scaffold.appBar]。
class AppPageBar extends StatelessWidget implements PreferredSizeWidget {
const AppPageBar({
super.key,
this.title = '',
this.titleWidget,
this.backgroundImage,
this.onBack,
this.actions,
});
final String title;
final Widget? titleWidget;
final String? backgroundImage;
final VoidCallback? onBack;
final List<Widget>? actions;
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
final imagePath = backgroundImage ?? Assets.images.imageAppBarBg.path;
return AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.light,
flexibleSpace: SizedBox.expand(
child: Image.asset(
imagePath,
fit: BoxFit.cover,
alignment: Alignment.center,
),
),
leading: IconButton(
icon: Icon(
Icons.chevron_left_rounded,
color: Colors.white,
size: 28.sp,
),
onPressed: onBack ?? () => AppNavigator.pop(context: context),
),
title:
titleWidget ??
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.w600,
fontFamily: 'PingFang SC',
),
),
actions: actions,
);
}
}
/// 兼容旧调用;内部委托 [AppPageBar]。
PreferredSizeWidget myAppBar({
required BuildContext context, required BuildContext context,
String title = '', String title = '',
Widget? titleWidget, Widget? titleWidget,
}) => AppBar( List<Widget>? actions,
leadingWidth: 56.w, }) {
leading: IconButton( return AppPageBar(title: title, titleWidget: titleWidget, actions: actions);
icon: const Icon(Icons.arrow_back), }
onPressed: () {
Navigator.of(context).maybePop();
},
),
title:
titleWidget ??
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontFamily: 'PingFang SC',
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
);
+1
View File
@@ -1,4 +1,5 @@
export 'app_avatar.dart'; export 'app_avatar.dart';
export 'app_bar.dart';
export 'app_button.dart'; export 'app_button.dart';
export 'app_card.dart'; export 'app_card.dart';
export 'app_dialog.dart'; export 'app_dialog.dart';