1.新增参赛队伍页面交互
2.新增人工处理功能交互
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
class CompetitionPlayer {
|
||||||
|
const CompetitionPlayer({required this.id, required this.name});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompetitionTeam {
|
||||||
|
const CompetitionTeam({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.players,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final List<CompetitionPlayer> players;
|
||||||
|
|
||||||
|
String get playerNames => players.map((player) => player.name).join('、');
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompetitionMatchup {
|
||||||
|
const CompetitionMatchup({
|
||||||
|
required this.id,
|
||||||
|
required this.teamA,
|
||||||
|
required this.teamB,
|
||||||
|
this.winnerTeamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final CompetitionTeam teamA;
|
||||||
|
final CompetitionTeam teamB;
|
||||||
|
final String? winnerTeamId;
|
||||||
|
|
||||||
|
CompetitionMatchup copyWith({String? winnerTeamId}) {
|
||||||
|
return CompetitionMatchup(
|
||||||
|
id: id,
|
||||||
|
teamA: teamA,
|
||||||
|
teamB: teamB,
|
||||||
|
winnerTeamId: winnerTeamId ?? this.winnerTeamId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompetitionTeamListItem {
|
||||||
|
const CompetitionTeamListItem({
|
||||||
|
required this.id,
|
||||||
|
required this.eventName,
|
||||||
|
required this.itemName,
|
||||||
|
required this.groupName,
|
||||||
|
required this.matchPlace,
|
||||||
|
required this.matchStartTime,
|
||||||
|
required this.matchEndTime,
|
||||||
|
required this.matchups,
|
||||||
|
this.completed = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String eventName;
|
||||||
|
final String itemName;
|
||||||
|
final String groupName;
|
||||||
|
final String matchPlace;
|
||||||
|
final String matchStartTime;
|
||||||
|
final String matchEndTime;
|
||||||
|
final List<CompetitionMatchup> matchups;
|
||||||
|
final bool completed;
|
||||||
|
|
||||||
|
String get title => groupName.isEmpty ? itemName : '$itemName ($groupName)';
|
||||||
|
|
||||||
|
String get scheduleTime => '$matchStartTime-$matchEndTime';
|
||||||
|
|
||||||
|
CompetitionTeamListItem copyWith({List<CompetitionMatchup>? matchups}) {
|
||||||
|
return CompetitionTeamListItem(
|
||||||
|
id: id,
|
||||||
|
eventName: eventName,
|
||||||
|
itemName: itemName,
|
||||||
|
groupName: groupName,
|
||||||
|
matchPlace: matchPlace,
|
||||||
|
matchStartTime: matchStartTime,
|
||||||
|
matchEndTime: matchEndTime,
|
||||||
|
matchups: matchups ?? this.matchups,
|
||||||
|
completed: completed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompetitionTeamPageResult {
|
||||||
|
const CompetitionTeamPageResult({
|
||||||
|
required this.items,
|
||||||
|
required this.total,
|
||||||
|
required this.page,
|
||||||
|
required this.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<CompetitionTeamListItem> items;
|
||||||
|
final int total;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
bool get hasMore => page * pageSize < total;
|
||||||
|
}
|
||||||
@@ -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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||||
|
|
||||||
|
final competitionTeamsServerProvider = Provider<CompetitionTeamsServer>((ref) {
|
||||||
|
return const CompetitionTeamsServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
class CompetitionTeamsServer {
|
||||||
|
const CompetitionTeamsServer();
|
||||||
|
|
||||||
|
static const totalCount = 10;
|
||||||
|
|
||||||
|
Future<CompetitionTeamPageResult> fetchPage({
|
||||||
|
required int page,
|
||||||
|
required int pageSize,
|
||||||
|
}) async {
|
||||||
|
final start = (page - 1) * pageSize;
|
||||||
|
if (start >= totalCount) {
|
||||||
|
return CompetitionTeamPageResult(
|
||||||
|
items: const [],
|
||||||
|
total: totalCount,
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final end = (start + pageSize).clamp(0, totalCount);
|
||||||
|
final items = List.generate(
|
||||||
|
end - start,
|
||||||
|
(index) => _buildItem(start + index),
|
||||||
|
);
|
||||||
|
return CompetitionTeamPageResult(
|
||||||
|
items: items,
|
||||||
|
total: totalCount,
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompetitionTeamListItem _buildItem(int index) {
|
||||||
|
final number = index + 1;
|
||||||
|
final group = index.isEven ? '小学组' : '初中组';
|
||||||
|
final hour = 9 + index ~/ 2;
|
||||||
|
return CompetitionTeamListItem(
|
||||||
|
id: 'competition-$number',
|
||||||
|
eventName: '全国青少年无人机大赛',
|
||||||
|
itemName: index % 3 == 0 ? '空中足球赛' : '空中格斗赛',
|
||||||
|
groupName: group,
|
||||||
|
matchPlace: '场地 ${index % 4 + 1}',
|
||||||
|
matchStartTime: '${hour.toString().padLeft(2, '0')}:00',
|
||||||
|
matchEndTime: '${hour.toString().padLeft(2, '0')}:30',
|
||||||
|
completed: index == totalCount - 1,
|
||||||
|
matchups: [
|
||||||
|
_buildMatchup(itemNumber: number, matchNumber: 1),
|
||||||
|
_buildMatchup(itemNumber: number, matchNumber: 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompetitionMatchup _buildMatchup({
|
||||||
|
required int itemNumber,
|
||||||
|
required int matchNumber,
|
||||||
|
}) {
|
||||||
|
final matchupId = 'match-$itemNumber-$matchNumber';
|
||||||
|
final teamAIndex = (itemNumber + matchNumber - 2) % _teamNames.length;
|
||||||
|
final teamBIndex = (teamAIndex + 1) % _teamNames.length;
|
||||||
|
return CompetitionMatchup(
|
||||||
|
id: matchupId,
|
||||||
|
teamA: _buildTeam(
|
||||||
|
id: '$matchupId-team-a',
|
||||||
|
name: _teamNames[teamAIndex],
|
||||||
|
playerOffset: teamAIndex * 3,
|
||||||
|
),
|
||||||
|
teamB: _buildTeam(
|
||||||
|
id: '$matchupId-team-b',
|
||||||
|
name: _teamNames[teamBIndex],
|
||||||
|
playerOffset: teamBIndex * 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompetitionTeam _buildTeam({
|
||||||
|
required String id,
|
||||||
|
required String name,
|
||||||
|
required int playerOffset,
|
||||||
|
}) {
|
||||||
|
return CompetitionTeam(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
players: List.generate(3, (index) {
|
||||||
|
final playerIndex = (playerOffset + index) % _playerNames.length;
|
||||||
|
return CompetitionPlayer(
|
||||||
|
id: '$id-player-$index',
|
||||||
|
name: _playerNames[playerIndex],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _teamNames = ['王多鱼队', '钱多多队', '逐风少年队', '蓝翼飞行队', '星火队'];
|
||||||
|
static const _playerNames = [
|
||||||
|
'王伟',
|
||||||
|
'李庆超',
|
||||||
|
'王大亮',
|
||||||
|
'韩宁政',
|
||||||
|
'阮晴桦',
|
||||||
|
'刘美玲',
|
||||||
|
'陈子航',
|
||||||
|
'周白芷',
|
||||||
|
'林培伦',
|
||||||
|
'蔡依婷',
|
||||||
|
'夏志豪',
|
||||||
|
'赵云飞',
|
||||||
|
'孙雨泽',
|
||||||
|
'吴佳琪',
|
||||||
|
'郑凯文',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||||
|
|
||||||
|
class CompetitionTeamsState {
|
||||||
|
const CompetitionTeamsState({
|
||||||
|
this.items = const [],
|
||||||
|
this.page = 0,
|
||||||
|
this.hasMore = true,
|
||||||
|
this.isInitialLoading = false,
|
||||||
|
this.isRefreshing = false,
|
||||||
|
this.isLoadingMore = false,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<CompetitionTeamListItem> items;
|
||||||
|
final int page;
|
||||||
|
final bool hasMore;
|
||||||
|
final bool isInitialLoading;
|
||||||
|
final bool isRefreshing;
|
||||||
|
final bool isLoadingMore;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
CompetitionTeamsState copyWith({
|
||||||
|
List<CompetitionTeamListItem>? items,
|
||||||
|
int? page,
|
||||||
|
bool? hasMore,
|
||||||
|
bool? isInitialLoading,
|
||||||
|
bool? isRefreshing,
|
||||||
|
bool? isLoadingMore,
|
||||||
|
String? errorMessage,
|
||||||
|
bool clearError = false,
|
||||||
|
}) {
|
||||||
|
return CompetitionTeamsState(
|
||||||
|
items: items ?? this.items,
|
||||||
|
page: page ?? this.page,
|
||||||
|
hasMore: hasMore ?? this.hasMore,
|
||||||
|
isInitialLoading: isInitialLoading ?? this.isInitialLoading,
|
||||||
|
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||||
|
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||||
|
errorMessage: clearError ? null : errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_riverpod/legacy.dart';
|
||||||
|
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||||
|
import 'package:recording_tool/features/competition_teams/server/server_competition_teams.dart';
|
||||||
|
import 'package:recording_tool/features/competition_teams/state/state_competition_teams.dart';
|
||||||
|
|
||||||
|
final competitionTeamsProvider =
|
||||||
|
StateNotifierProvider<CompetitionTeamsViewModel, CompetitionTeamsState>((
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return CompetitionTeamsViewModel(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
class CompetitionTeamsViewModel extends StateNotifier<CompetitionTeamsState> {
|
||||||
|
CompetitionTeamsViewModel(this._ref) : super(const CompetitionTeamsState());
|
||||||
|
|
||||||
|
static const pageSize = 5;
|
||||||
|
|
||||||
|
final Ref _ref;
|
||||||
|
|
||||||
|
Future<void> loadInitial() async {
|
||||||
|
state = state.copyWith(
|
||||||
|
isInitialLoading: true,
|
||||||
|
isLoadingMore: false,
|
||||||
|
isRefreshing: false,
|
||||||
|
clearError: true,
|
||||||
|
);
|
||||||
|
await _loadPage(page: 1, replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
if (state.isRefreshing) return;
|
||||||
|
state = state.copyWith(isRefreshing: true, clearError: true);
|
||||||
|
await _loadPage(page: 1, replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadMore() async {
|
||||||
|
if (!state.hasMore || state.isLoadingMore || state.isInitialLoading) return;
|
||||||
|
state = state.copyWith(isLoadingMore: true, clearError: true);
|
||||||
|
await _loadPage(page: state.page + 1, replace: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectWinner({
|
||||||
|
required String itemId,
|
||||||
|
required String matchupId,
|
||||||
|
required String winnerTeamId,
|
||||||
|
}) {
|
||||||
|
final updatedItems = state.items
|
||||||
|
.map((item) {
|
||||||
|
if (item.id != itemId) return item;
|
||||||
|
final updatedMatchups = item.matchups
|
||||||
|
.map((matchup) {
|
||||||
|
if (matchup.id != matchupId) return matchup;
|
||||||
|
final validWinner =
|
||||||
|
winnerTeamId == matchup.teamA.id ||
|
||||||
|
winnerTeamId == matchup.teamB.id;
|
||||||
|
return validWinner
|
||||||
|
? matchup.copyWith(winnerTeamId: winnerTeamId)
|
||||||
|
: matchup;
|
||||||
|
})
|
||||||
|
.toList(growable: false);
|
||||||
|
return item.copyWith(matchups: updatedMatchups);
|
||||||
|
})
|
||||||
|
.toList(growable: false);
|
||||||
|
state = state.copyWith(items: updatedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompetitionTeamListItem? findItem(String itemId) {
|
||||||
|
for (final item in state.items) {
|
||||||
|
if (item.id == itemId) return item;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPage({required int page, required bool replace}) async {
|
||||||
|
try {
|
||||||
|
final result = await _ref
|
||||||
|
.read(competitionTeamsServerProvider)
|
||||||
|
.fetchPage(page: page, pageSize: pageSize);
|
||||||
|
state = state.copyWith(
|
||||||
|
items: replace ? result.items : [...state.items, ...result.items],
|
||||||
|
page: result.page,
|
||||||
|
hasMore: result.hasMore,
|
||||||
|
isInitialLoading: false,
|
||||||
|
isRefreshing: false,
|
||||||
|
isLoadingMore: false,
|
||||||
|
clearError: true,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isInitialLoading: false,
|
||||||
|
isRefreshing: false,
|
||||||
|
isLoadingMore: false,
|
||||||
|
errorMessage: '参赛队伍加载失败,请重试',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||||
|
import 'package:recording_tool/shared/widgets/app_button.dart';
|
||||||
|
|
||||||
|
class ManualWinnerDialog extends StatefulWidget {
|
||||||
|
const ManualWinnerDialog({super.key, required this.matchup});
|
||||||
|
|
||||||
|
final CompetitionMatchup matchup;
|
||||||
|
|
||||||
|
static Future<String?> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required CompetitionMatchup matchup,
|
||||||
|
}) {
|
||||||
|
return showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => ManualWinnerDialog(matchup: matchup),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ManualWinnerDialog> createState() => _ManualWinnerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ManualWinnerDialogState extends State<ManualWinnerDialog> {
|
||||||
|
String? _selectedTeamId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedTeamId = widget.matchup.winnerTeamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
insetPadding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.r)),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 320.w,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/image_dialog_bg.png',
|
||||||
|
width: 320.w,
|
||||||
|
height: 112.h,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 20.h),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'请在比赛结束前处理',
|
||||||
|
key: const ValueKey('manual-winner-dialog-title'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20.sp,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: const Color(0xFF20242B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
_TeamChoice(
|
||||||
|
team: widget.matchup.teamA,
|
||||||
|
color: const Color(0xFFFF6B75),
|
||||||
|
selected: _selectedTeamId == widget.matchup.teamA.id,
|
||||||
|
onTap: () => setState(
|
||||||
|
() => _selectedTeamId = widget.matchup.teamA.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.h),
|
||||||
|
_TeamChoice(
|
||||||
|
team: widget.matchup.teamB,
|
||||||
|
color: const Color(0xFF20BFA9),
|
||||||
|
selected: _selectedTeamId == widget.matchup.teamB.id,
|
||||||
|
onTap: () => setState(
|
||||||
|
() => _selectedTeamId = widget.matchup.teamB.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AppButton(
|
||||||
|
label: '取消',
|
||||||
|
variant: AppButtonVariant.secondary,
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14.w),
|
||||||
|
Expanded(
|
||||||
|
child: AppButton(
|
||||||
|
label: '确定',
|
||||||
|
onPressed: _selectedTeamId == null
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(_selectedTeamId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TeamChoice extends StatelessWidget {
|
||||||
|
const _TeamChoice({
|
||||||
|
required this.team,
|
||||||
|
required this.color,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CompetitionTeam team;
|
||||||
|
final Color color;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Semantics(
|
||||||
|
selected: selected,
|
||||||
|
button: true,
|
||||||
|
label: '选择${team.name}直接获胜',
|
||||||
|
child: Material(
|
||||||
|
color: color.withValues(alpha: selected ? 0.16 : 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
child: InkWell(
|
||||||
|
key: ValueKey('winner-choice-${team.id}'),
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? color : color.withValues(alpha: 0.35),
|
||||||
|
width: selected ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 160),
|
||||||
|
width: 22.r,
|
||||||
|
height: 22.r,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: selected ? color : Colors.grey),
|
||||||
|
color: selected ? color : Colors.transparent,
|
||||||
|
),
|
||||||
|
child: selected
|
||||||
|
? Icon(Icons.check, size: 15.r, color: Colors.white)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12.w),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
team.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: const Color(0xFF20242B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
Text(
|
||||||
|
team.playerNames,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.sp,
|
||||||
|
color: const Color(0xFF606874),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Text(
|
||||||
|
'直接获胜',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:recording_tool/core/cache/app_storage.dart';
|
|||||||
import 'package:recording_tool/core/cache/storage_keys.dart';
|
import 'package:recording_tool/core/cache/storage_keys.dart';
|
||||||
import 'package:recording_tool/features/auth/pages/page_auth.dart';
|
import 'package:recording_tool/features/auth/pages/page_auth.dart';
|
||||||
import 'package:recording_tool/features/auth/view_model_auth/view_model_auth.dart';
|
import 'package:recording_tool/features/auth/view_model_auth/view_model_auth.dart';
|
||||||
|
import 'package:recording_tool/features/competition_teams/pages/page_competition_team_list.dart';
|
||||||
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/pages/page_event_info.dart';
|
import 'package:recording_tool/features/events/pages/page_event_info.dart';
|
||||||
import 'package:recording_tool/features/events/view_model/view_model_event_info.dart';
|
import 'package:recording_tool/features/events/view_model/view_model_event_info.dart';
|
||||||
@@ -112,7 +113,12 @@ class _AuthPageWidgetState extends ConsumerState<ScanQrCodePage> {
|
|||||||
height: 80.h,
|
height: 80.h,
|
||||||
child: AppButton(
|
child: AppButton(
|
||||||
label: '参赛队伍',
|
label: '参赛队伍',
|
||||||
onPressed: () async {},
|
onPressed: () async {
|
||||||
|
await AppNavigator.push(
|
||||||
|
const CompetitionTeamListPage(),
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
},
|
||||||
variant: AppButtonVariant.secondary,
|
variant: AppButtonVariant.secondary,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user