完成选手信息页

This commit is contained in:
2026-07-27 17:57:09 +08:00
parent 356636c4b4
commit 5ffe524e20
8 changed files with 426 additions and 201 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

+339 -176
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.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 profile = state.profile;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light.copyWith(
statusBarColor: Colors.transparent,
systemNavigationBarColor: const Color(0xFFF5F6F8),
),
child: Scaffold(
backgroundColor: const Color(0xFFF5F6F8),
appBar: myAppBar(context: context),
body: Stack(
children: [
_Header(onBack: () => AppNavigator.pop(context: context)),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(62.w, 8.h, 62.w, 40.h),
child: Column(
children: [
_ProfileSection(profile: profile),
SizedBox(height: 20.h),
Text(
state.eventTitle,
style: TextStyle(
fontSize: 20.sp,
height: 1.2,
color: const Color(0xFF2F2F2F),
fontWeight: FontWeight.w500,
),
const _TopGradientHeader(),
SafeArea(
bottom: false,
child: Column(
children: [
// _Header(onBack: () => AppNavigator.pop(context: context)),
SizedBox(height: 10.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: _ProfileSection(
profile: profile,
eventTitle: state.eventTitle,
),
SizedBox(height: 24.h),
SizedBox(
height: 400.h,
child: _ScheduleList(
state: state,
onRetry: () => ref
.read(eventInfoProvider.notifier)
.loadRegistrationList(),
onItemTap: onItemTap,
),
),
SizedBox(height: 8.h),
Expanded(
child: _ScheduleList(
state: state,
onRetry: () => ref
.read(eventInfoProvider.notifier)
.loadRegistrationList(),
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({
required EventRegistrationItem item,
required String playerId,
@@ -135,16 +160,16 @@ class _Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 52.h,
height: 44.h,
child: Align(
alignment: Alignment.centerLeft,
child: IconButton(
onPressed: onBack,
icon: Icon(Icons.arrow_back_ios_new, size: 32.r),
color: Colors.black,
icon: Icon(Icons.chevron_left_rounded, size: 28.r),
color: Colors.white,
tooltip: '返回',
padding: EdgeInsets.only(left: 16.w),
constraints: BoxConstraints(minWidth: 56.w, minHeight: 52.h),
padding: EdgeInsets.only(left: 10.w),
constraints: BoxConstraints(minWidth: 44.w, minHeight: 44.h),
),
),
);
@@ -152,52 +177,143 @@ class _Header extends StatelessWidget {
}
class _ProfileSection extends StatelessWidget {
const _ProfileSection({required this.profile});
const _ProfileSection({required this.profile, required this.eventTitle});
final EventProfile profile;
final String eventTitle;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
profile.avatarUrl.isEmpty
? AppAvatar(size: 50.r)
: AppAvatar(size: 50.r, imageUrl: profile.avatarUrl),
SizedBox(width: 28.w),
Expanded(
child: Padding(
padding: EdgeInsets.only(top: 30.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 22.sp,
height: 1.2,
color: const Color(0xFF2F2F2F),
final title = eventTitle.trim().isEmpty ? '赛事信息' : eventTitle.trim();
return Container(
clipBehavior: Clip.antiAlias,
padding: EdgeInsets.fromLTRB(12.w, 10.h, 12.w, 12.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
),
child: Stack(
children: [
// Positioned(
// top: -28.h,
// right: -14.w,
// child: Transform.rotate(
// angle: 0.68,
// child: Container(
// width: 110.w,
// height: 138.h,
// decoration: BoxDecoration(
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// 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(
profile.phone,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 22.sp,
height: 1.2,
color: const Color(0xFF2F2F2F),
),
),
],
),
],
),
SizedBox(height: 10.h),
Divider(
height: 1.h,
thickness: 1.h,
color: const Color(0xFFE8EAED),
),
SizedBox(height: 10.h),
_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(
'暂无赛事报名信息',
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('重新加载')),
],
),
);
}
return ListView.builder(
return ListView.separated(
padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 24.h),
itemCount: state.items.length,
separatorBuilder: (_, _) => SizedBox(height: 8.h),
itemBuilder: (context, 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 {
const _ScheduleCard({required this.item, required this.onTap});
const _ScheduleCard({
required this.item,
required this.scheduleIndex,
required this.onTap,
});
final EventRegistrationItem item;
final int scheduleIndex;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(6.r),
child: Container(
constraints: BoxConstraints(minHeight: 138.h),
padding: EdgeInsets.fromLTRB(10.w, 14.h, 14.w, 0),
constraints: BoxConstraints(minHeight: 86.h),
padding: EdgeInsets.fromLTRB(12.w, 10.h, 14.w, 10.h),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF7A7A7A)),
color: Colors.white,
borderRadius: BorderRadius.circular(6.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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),
),
),
],
child: _ScheduleDetails(
item: item,
scheduleIndex: scheduleIndex,
),
),
SizedBox(width: 12.w),
SizedBox(
width: 148.w,
height: 120.h,
child: Align(
alignment: Alignment.center,
child: _ScheduleBadge(item: item),
),
),
_ScheduleStatus(item: item),
],
),
),
@@ -322,72 +409,148 @@ class _ScheduleCard extends StatelessWidget {
}
}
class _ScheduleBadge extends StatelessWidget {
const _ScheduleBadge({required this.item});
class _ScheduleDetails extends StatelessWidget {
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;
@override
Widget build(BuildContext context) {
if (item.statusLabel != null) {
return CustomPaint(
painter: _CutCornerBorderPainter(color: const Color(0xFF7A7A7A)),
child: SizedBox(
width: 148.w,
height: 90.h,
child: Center(
child: Text(
item.statusLabel!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 20.sp, color: const Color(0xFF2F2F2F)),
),
),
final status = item.statusLabel;
if (status != null) {
return Text(
status,
style: TextStyle(
fontSize: 10.sp,
height: 1.2,
color: const Color(0xFF1DBF73),
fontWeight: FontWeight.w500,
),
);
}
return Text(
item.matchPlace,
_formatNumberBadge(item),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 34.sp,
fontSize: 20.sp,
height: 1,
color: const Color(0xFF2F2F2F),
color: const Color(0xFFF27D69),
fontWeight: FontWeight.w700,
),
);
}
}
class _CutCornerBorderPainter extends CustomPainter {
const _CutCornerBorderPainter({required this.color});
String _formatTitle(EventRegistrationItem item) {
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
void paint(Canvas canvas, Size size) {
final side = 12.r;
final path = Path()
..moveTo(side, 0)
..lineTo(size.width - side, 0)
..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);
String _formatScheduleTime(EventRegistrationItem item) {
final start = DateTime.tryParse(item.matchStartTime);
final end = DateTime.tryParse(item.matchEndTime);
if (start == null && end == null) return item.scheduleTime;
if (start != null && end != null) {
return '${start.month}${start.day}${_formatClock(start)}-${_formatClock(end)}';
}
final value = start ?? end!;
return '${value.month}${value.day}${_formatClock(value)}';
}
@override
bool shouldRepaint(covariant _CutCornerBorderPainter oldDelegate) {
return oldDelegate.color != color;
}
String _formatClock(DateTime value) {
return '${value.hour}:${value.minute.toString().padLeft(2, '0')}';
}
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;
}
+15
View File
@@ -24,6 +24,10 @@ class $AssetsHtmlGen {
class $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
AssetGenImage get imageAppIcon =>
const AssetGenImage('assets/images/image_app_icon.png');
@@ -40,10 +44,18 @@ class $AssetsImagesGen {
AssetGenImage get imageLogo =>
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
AssetGenImage get imagePageBg =>
const AssetGenImage('assets/images/image_page_bg.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
AssetGenImage get imageScanQrcode =>
const AssetGenImage('assets/images/image_scan_qrcode.png');
@@ -54,11 +66,14 @@ class $AssetsImagesGen {
/// List of all assets
List<AssetGenImage> get values => [
imageAppBarBg,
imageAppIcon,
imageAppNameText,
imageDialogBg,
imageLogo,
imageOk,
imagePageBg,
imageScan,
imageScanQrcode,
imageVs,
];
+71 -25
View File
@@ -1,31 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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,
String title = '',
Widget? titleWidget,
}) => AppBar(
leadingWidth: 56.w,
leading: IconButton(
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,
);
List<Widget>? actions,
}) {
return AppPageBar(title: title, titleWidget: titleWidget, actions: actions);
}
+1
View File
@@ -1,4 +1,5 @@
export 'app_avatar.dart';
export 'app_bar.dart';
export 'app_button.dart';
export 'app_card.dart';
export 'app_dialog.dart';