1.新增参赛队伍页面交互

2.新增人工处理功能交互
This commit is contained in:
2026-07-20 09:29:53 +08:00
parent a4333d3bd1
commit 4e0a675198
8 changed files with 1014 additions and 1 deletions
@@ -0,0 +1,249 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
import 'package:recording_tool/features/competition_teams/view_model/view_model_competition_teams.dart';
import 'package:recording_tool/features/competition_teams/widgets/widget_manual_winner_dialog.dart';
import 'package:recording_tool/shared/widgets/app_empty_view.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart';
class CompetitionTeamDetailPage extends ConsumerWidget {
const CompetitionTeamDetailPage({super.key, required this.itemId});
final String itemId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(
competitionTeamsProvider.select((state) => state.items),
);
CompetitionTeamListItem? item;
for (final candidate in items) {
if (candidate.id == itemId) {
item = candidate;
break;
}
}
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text(item?.title ?? '参赛队伍')),
body: item == null
? const AppEmptyView(message: '未找到对阵信息')
: ListView.separated(
padding: EdgeInsets.fromLTRB(20.w, 24.h, 20.w, 36.h),
itemCount: item.matchups.length,
separatorBuilder: (_, _) => SizedBox(height: 22.h),
itemBuilder: (context, index) {
final matchup = item!.matchups[index];
return _MatchupCard(
matchup: matchup,
index: index,
onManualProcess: () => _handleManualProcess(
context,
ref,
itemId: item!.id,
matchup: matchup,
),
);
},
),
);
}
Future<void> _handleManualProcess(
BuildContext context,
WidgetRef ref, {
required String itemId,
required CompetitionMatchup matchup,
}) async {
final winnerTeamId = await ManualWinnerDialog.show(
context,
matchup: matchup,
);
if (winnerTeamId == null || !context.mounted) return;
ref
.read(competitionTeamsProvider.notifier)
.selectWinner(
itemId: itemId,
matchupId: matchup.id,
winnerTeamId: winnerTeamId,
);
final winnerName = winnerTeamId == matchup.teamA.id
? matchup.teamA.name
: matchup.teamB.name;
AppToast.show('已设置$winnerName直接获胜');
}
}
class _MatchupCard extends StatelessWidget {
const _MatchupCard({
required this.matchup,
required this.index,
required this.onManualProcess,
});
final CompetitionMatchup matchup;
final int index;
final VoidCallback onManualProcess;
@override
Widget build(BuildContext context) {
return Container(
key: ValueKey('competition-matchup-${matchup.id}'),
padding: EdgeInsets.fromLTRB(16.w, 10.h, 16.w, 18.h),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFC7CCD4)),
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
Row(
children: [
Text(
'${index + 1}',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF7A828E),
),
),
const Spacer(),
TextButton(
key: ValueKey('manual-process-${matchup.id}'),
onPressed: onManualProcess,
child: Text(
'人工处理',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF078AF2),
),
),
),
],
),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: _TeamPanel(
team: matchup.teamA,
alignment: CrossAxisAlignment.start,
winner: matchup.winnerTeamId == matchup.teamA.id,
accentColor: const Color(0xFFFF6B75),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Text(
'VS',
style: TextStyle(
fontSize: 21.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF303640),
),
),
),
Expanded(
child: _TeamPanel(
team: matchup.teamB,
alignment: CrossAxisAlignment.end,
textAlign: TextAlign.end,
winner: matchup.winnerTeamId == matchup.teamB.id,
accentColor: const Color(0xFF20BFA9),
),
),
],
),
],
),
);
}
}
class _TeamPanel extends StatelessWidget {
const _TeamPanel({
required this.team,
required this.alignment,
required this.winner,
required this.accentColor,
this.textAlign = TextAlign.start,
});
final CompetitionTeam team;
final CrossAxisAlignment alignment;
final TextAlign textAlign;
final bool winner;
final Color accentColor;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: alignment,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: winner
? accentColor.withValues(alpha: 0.14)
: Colors.transparent,
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
team.name,
textAlign: textAlign,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF282E37),
),
),
),
if (winner) ...[
SizedBox(width: 5.w),
Icon(Icons.emoji_events, size: 17.r, color: accentColor),
],
],
),
),
SizedBox(height: 10.h),
Text(
team.playerNames,
textAlign: textAlign,
style: TextStyle(
fontSize: 15.sp,
height: 1.45,
color: const Color(0xFF4E5662),
),
),
if (winner) ...[
SizedBox(height: 8.h),
Container(
key: ValueKey('winner-${team.id}'),
padding: EdgeInsets.symmetric(horizontal: 9.w, vertical: 3.h),
decoration: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(10.r),
),
child: Text(
'胜方',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
],
],
);
}
}
@@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/app/router/app_navigator.dart';
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
import 'package:recording_tool/features/competition_teams/pages/page_competition_team_detail.dart';
import 'package:recording_tool/features/competition_teams/view_model/view_model_competition_teams.dart';
import 'package:recording_tool/shared/widgets/app_empty_view.dart';
import 'package:recording_tool/shared/widgets/app_error_view.dart';
import 'package:recording_tool/shared/widgets/app_loading_view.dart';
import 'package:recording_tool/shared/widgets/app_refresh_list.dart';
class CompetitionTeamListPage extends ConsumerStatefulWidget {
const CompetitionTeamListPage({super.key});
@override
ConsumerState<CompetitionTeamListPage> createState() =>
_CompetitionTeamListPageState();
}
class _CompetitionTeamListPageState
extends ConsumerState<CompetitionTeamListPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(competitionTeamsProvider.notifier).loadInitial();
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(competitionTeamsProvider);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: const Text('参赛队伍')),
body: SafeArea(
top: false,
child: Builder(
builder: (context) {
if (state.isInitialLoading && state.items.isEmpty) {
return const AppLoadingView(message: '正在加载参赛队伍...');
}
if (state.errorMessage != null && state.items.isEmpty) {
return AppErrorView(
message: state.errorMessage!,
onRetry: () =>
ref.read(competitionTeamsProvider.notifier).loadInitial(),
);
}
return AppRefreshList<CompetitionTeamListItem>(
items: state.items,
onRefresh: ref.read(competitionTeamsProvider.notifier).refresh,
onLoadMore: ref.read(competitionTeamsProvider.notifier).loadMore,
enablePullUp: state.hasMore,
padding: EdgeInsets.fromLTRB(20.w, 18.h, 20.w, 28.h),
separator: SizedBox(height: 14.h),
empty: const AppEmptyView(message: '暂无参赛队伍'),
itemBuilder: (context, item, index) {
return _CompetitionScheduleCard(
item: item,
onTap: () => AppNavigator.push(
CompetitionTeamDetailPage(itemId: item.id),
context: context,
),
);
},
);
},
),
),
);
}
}
class _CompetitionScheduleCard extends StatelessWidget {
const _CompetitionScheduleCard({required this.item, required this.onTap});
final CompetitionTeamListItem item;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
key: ValueKey('competition-team-item-${item.id}'),
color: Colors.white,
borderRadius: BorderRadius.circular(14.r),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14.r),
child: Container(
constraints: BoxConstraints(minHeight: 142.h),
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.r),
border: Border.all(color: const Color(0xFFD7DBE2)),
boxShadow: const [
BoxShadow(
color: Color(0x0F1A2230),
blurRadius: 16,
offset: Offset(0, 6),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 19.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF20242B),
),
),
SizedBox(height: 18.h),
_InfoLine(
icon: Icons.location_on_outlined,
text: item.matchPlace,
),
SizedBox(height: 12.h),
_InfoLine(
icon: Icons.schedule_outlined,
text: item.scheduleTime,
),
],
),
),
SizedBox(width: 14.w),
Container(
width: 72.w,
height: 72.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: item.completed
? const Color(0xFFF1F3F6)
: const Color(0xFFEAF3FF),
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
item.completed ? '已完成' : item.matchPlace,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: item.completed ? 14.sp : 20.sp,
fontWeight: FontWeight.w700,
color: item.completed
? const Color(0xFF69717D)
: const Color(0xFF147FEA),
),
),
),
],
),
),
),
);
}
}
class _InfoLine extends StatelessWidget {
const _InfoLine({required this.icon, required this.text});
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, size: 18.r, color: const Color(0xFF7B8491)),
SizedBox(width: 8.w),
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 15.sp, color: const Color(0xFF525B68)),
),
),
],
);
}
}