From 4e0a6751983773075ce2f0d64fa25b31bfc4a241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E9=94=8B?= <2535831261@qq.com> Date: Mon, 20 Jul 2026 09:29:53 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=96=B0=E5=A2=9E=E5=8F=82=E8=B5=9B=E9=98=9F?= =?UTF-8?q?=E4=BC=8D=E9=A1=B5=E9=9D=A2=E4=BA=A4=E4=BA=92=202.=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=BA=BA=E5=B7=A5=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/model_competition_team.dart | 101 +++++++ .../pages/page_competition_team_detail.dart | 249 ++++++++++++++++++ .../pages/page_competition_team_list.dart | 189 +++++++++++++ .../server/server_competition_teams.dart | 118 +++++++++ .../state/state_competition_teams.dart | 42 +++ .../view_model_competition_teams.dart | 98 +++++++ .../widgets/widget_manual_winner_dialog.dart | 210 +++++++++++++++ .../scan_qrcode/pages/page_scan_qrcode.dart | 8 +- 8 files changed, 1014 insertions(+), 1 deletion(-) create mode 100644 lib/features/competition_teams/model/model_competition_team.dart create mode 100644 lib/features/competition_teams/pages/page_competition_team_detail.dart create mode 100644 lib/features/competition_teams/pages/page_competition_team_list.dart create mode 100644 lib/features/competition_teams/server/server_competition_teams.dart create mode 100644 lib/features/competition_teams/state/state_competition_teams.dart create mode 100644 lib/features/competition_teams/view_model/view_model_competition_teams.dart create mode 100644 lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart diff --git a/lib/features/competition_teams/model/model_competition_team.dart b/lib/features/competition_teams/model/model_competition_team.dart new file mode 100644 index 0000000..5afa5ce --- /dev/null +++ b/lib/features/competition_teams/model/model_competition_team.dart @@ -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 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 matchups; + final bool completed; + + String get title => groupName.isEmpty ? itemName : '$itemName ($groupName)'; + + String get scheduleTime => '$matchStartTime-$matchEndTime'; + + CompetitionTeamListItem copyWith({List? 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 items; + final int total; + final int page; + final int pageSize; + + bool get hasMore => page * pageSize < total; +} diff --git a/lib/features/competition_teams/pages/page_competition_team_detail.dart b/lib/features/competition_teams/pages/page_competition_team_detail.dart new file mode 100644 index 0000000..398b103 --- /dev/null +++ b/lib/features/competition_teams/pages/page_competition_team_detail.dart @@ -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 _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, + ), + ), + ), + ], + ], + ); + } +} diff --git a/lib/features/competition_teams/pages/page_competition_team_list.dart b/lib/features/competition_teams/pages/page_competition_team_list.dart new file mode 100644 index 0000000..98f5d86 --- /dev/null +++ b/lib/features/competition_teams/pages/page_competition_team_list.dart @@ -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 createState() => + _CompetitionTeamListPageState(); +} + +class _CompetitionTeamListPageState + extends ConsumerState { + @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( + 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)), + ), + ), + ], + ); + } +} diff --git a/lib/features/competition_teams/server/server_competition_teams.dart b/lib/features/competition_teams/server/server_competition_teams.dart new file mode 100644 index 0000000..3404f48 --- /dev/null +++ b/lib/features/competition_teams/server/server_competition_teams.dart @@ -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((ref) { + return const CompetitionTeamsServer(); +}); + +class CompetitionTeamsServer { + const CompetitionTeamsServer(); + + static const totalCount = 10; + + Future 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 = [ + '王伟', + '李庆超', + '王大亮', + '韩宁政', + '阮晴桦', + '刘美玲', + '陈子航', + '周白芷', + '林培伦', + '蔡依婷', + '夏志豪', + '赵云飞', + '孙雨泽', + '吴佳琪', + '郑凯文', + ]; +} diff --git a/lib/features/competition_teams/state/state_competition_teams.dart b/lib/features/competition_teams/state/state_competition_teams.dart new file mode 100644 index 0000000..52a15de --- /dev/null +++ b/lib/features/competition_teams/state/state_competition_teams.dart @@ -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 items; + final int page; + final bool hasMore; + final bool isInitialLoading; + final bool isRefreshing; + final bool isLoadingMore; + final String? errorMessage; + + CompetitionTeamsState copyWith({ + List? 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, + ); + } +} diff --git a/lib/features/competition_teams/view_model/view_model_competition_teams.dart b/lib/features/competition_teams/view_model/view_model_competition_teams.dart new file mode 100644 index 0000000..b57a4de --- /dev/null +++ b/lib/features/competition_teams/view_model/view_model_competition_teams.dart @@ -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(( + ref, + ) { + return CompetitionTeamsViewModel(ref); + }); + +class CompetitionTeamsViewModel extends StateNotifier { + CompetitionTeamsViewModel(this._ref) : super(const CompetitionTeamsState()); + + static const pageSize = 5; + + final Ref _ref; + + Future loadInitial() async { + state = state.copyWith( + isInitialLoading: true, + isLoadingMore: false, + isRefreshing: false, + clearError: true, + ); + await _loadPage(page: 1, replace: true); + } + + Future refresh() async { + if (state.isRefreshing) return; + state = state.copyWith(isRefreshing: true, clearError: true); + await _loadPage(page: 1, replace: true); + } + + Future 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 _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: '参赛队伍加载失败,请重试', + ); + } + } +} diff --git a/lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart b/lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart new file mode 100644 index 0000000..1d77e95 --- /dev/null +++ b/lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart @@ -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 show( + BuildContext context, { + required CompetitionMatchup matchup, + }) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (_) => ManualWinnerDialog(matchup: matchup), + ); + } + + @override + State createState() => _ManualWinnerDialogState(); +} + +class _ManualWinnerDialogState extends State { + 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, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart index d383fec..8c4eee5 100644 --- a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart +++ b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart @@ -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/features/auth/pages/page_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/pages/page_event_info.dart'; import 'package:recording_tool/features/events/view_model/view_model_event_info.dart'; @@ -112,7 +113,12 @@ class _AuthPageWidgetState extends ConsumerState { height: 80.h, child: AppButton( label: '参赛队伍', - onPressed: () async {}, + onPressed: () async { + await AppNavigator.push( + const CompetitionTeamListPage(), + context: context, + ); + }, variant: AppButtonVariant.secondary, ), );