From 10d9eee60e7cbfe4197d0e636805871f85afc059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E9=94=8B?= <2535831261@qq.com> Date: Thu, 23 Jul 2026 15:35:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=BA=E5=B7=A5=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=99=8B=E7=BA=A7/=E6=B7=98=E6=B1=B0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=A8=A1=E5=9E=8B=E5=92=8CAPI=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=9B=E9=87=8D=E6=9E=84=E6=AF=94=E8=B5=9B=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7=E4=BA=A4=E4=BA=92=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/config/api_common.dart | 3 + .../model/model_competition_team.dart | 158 ---------------- .../model/model_competition_team_detail.dart | 174 ++++++++++++++++++ .../pages/page_competition_team_detail.dart | 107 ++++++++--- .../pages/page_event_team_match.dart | 102 +++++++--- .../server/server_competition_teams.dart | 1 + .../widgets/widget_manual_winner_dialog.dart | 112 +++++++---- .../events/pages/page_event_info.dart | 2 +- .../request_model/request_model_event.dart | 58 ++++++ lib/features/events/server/server_events.dart | 11 ++ 10 files changed, 484 insertions(+), 244 deletions(-) create mode 100644 lib/features/competition_teams/model/model_competition_team_detail.dart create mode 100644 lib/features/events/request_model/request_model_event.dart diff --git a/lib/app/config/api_common.dart b/lib/app/config/api_common.dart index 89e92c4..3b84cb3 100644 --- a/lib/app/config/api_common.dart +++ b/lib/app/config/api_common.dart @@ -14,6 +14,9 @@ enum AuthApi { /// 参赛队伍详情 getTeamDetail('/api/events/device/schedule/pending'), + /// 人工设置晋级/淘汰 + setTeamStatus('/api/events/device/schedule/team/score'), + /// 获取选手的赛事信息 playerRegistrationList('/api/events/device/player/registration/list'); diff --git a/lib/features/competition_teams/model/model_competition_team.dart b/lib/features/competition_teams/model/model_competition_team.dart index b307257..499961f 100644 --- a/lib/features/competition_teams/model/model_competition_team.dart +++ b/lib/features/competition_teams/model/model_competition_team.dart @@ -169,161 +169,3 @@ class CompetitionTeamPageResult { ); } } - -/// 参赛队伍详情 -class CompetitionTeamDetail { - String? scheduleId; - String? itemId; - String? eventId; - String? itemName; - String? groupName; - String? matchPlace; - String? matchStartTime; - String? matchEndTime; - List? matchups; - - CompetitionTeamDetail({ - this.scheduleId, - this.itemId, - this.eventId, - this.itemName, - this.groupName, - this.matchPlace, - this.matchStartTime, - this.matchEndTime, - this.matchups, - }); - - factory CompetitionTeamDetail.fromJson(Map json) => - CompetitionTeamDetail( - scheduleId: json['scheduleId']?.toString(), - itemId: json['itemId']?.toString(), - eventId: json['eventId']?.toString(), - itemName: json['itemName']?.toString(), - groupName: json['groupName']?.toString(), - matchPlace: json['matchPlace']?.toString(), - matchStartTime: json['matchStartTime']?.toString(), - matchEndTime: json['matchEndTime']?.toString(), - matchups: json['matchups'] == null - ? const [] - : List.from( - (json['matchups'] as List).whereType().map( - (x) => Matchup.fromJson(Map.from(x)), - ), - ), - ); - - /// 兼容 data 为对象或数组(取首项) - factory CompetitionTeamDetail.fromResponse(dynamic json) { - if (json is List) { - if (json.isEmpty) return CompetitionTeamDetail(matchups: const []); - final first = json.first; - if (first is Map) { - return CompetitionTeamDetail.fromJson(Map.from(first)); - } - return CompetitionTeamDetail(matchups: const []); - } - if (json is Map) { - return CompetitionTeamDetail.fromJson(Map.from(json)); - } - return CompetitionTeamDetail(matchups: const []); - } - - Map toJson() => { - 'scheduleId': scheduleId, - 'itemId': itemId, - 'eventId': eventId, - 'itemName': itemName, - 'groupName': groupName, - 'matchPlace': matchPlace, - 'matchStartTime': matchStartTime, - 'matchEndTime': matchEndTime, - 'matchups': matchups == null - ? [] - : List.from(matchups!.map((x) => x.toJson())), - }; -} - -class Matchup { - String? matchTitle; - List? teamA; - List? teamB; - - Matchup({this.matchTitle, this.teamA, this.teamB}); - - factory Matchup.fromJson(Map json) => Matchup( - matchTitle: json['matchTitle']?.toString(), - teamA: json['teamA'] == null - ? const [] - : List.from( - (json['teamA'] as List).whereType().map( - (x) => Team.fromJson(Map.from(x)), - ), - ), - teamB: json['teamB'] == null - ? const [] - : List.from( - (json['teamB'] as List).whereType().map( - (x) => Team.fromJson(Map.from(x)), - ), - ), - ); - - Map toJson() => { - 'matchTitle': matchTitle, - 'teamA': teamA == null - ? [] - : List.from(teamA!.map((x) => x.toJson())), - 'teamB': teamB == null - ? [] - : List.from(teamB!.map((x) => x.toJson())), - }; -} - -class Team { - String? id; - String? teamName; - List? players; - - Team({this.id, this.teamName, this.players}); - - factory Team.fromJson(Map json) => Team( - id: json['id']?.toString(), - teamName: json['teamName']?.toString(), - players: json['players'] == null - ? const [] - : List.from( - (json['players'] as List).whereType().map( - (x) => Player.fromJson(Map.from(x)), - ), - ), - ); - - Map toJson() => { - 'id': id, - 'teamName': teamName, - 'players': players == null - ? [] - : List.from(players!.map((x) => x.toJson())), - }; -} - -class Player { - String? id; - String? name; - bool? isLeader; - - Player({this.id, this.name, this.isLeader}); - - factory Player.fromJson(Map json) => Player( - id: json['id']?.toString(), - name: json['name']?.toString(), - isLeader: json['isLeader'] as bool?, - ); - - Map toJson() => { - 'id': id, - 'name': name, - 'isLeader': isLeader, - }; -} diff --git a/lib/features/competition_teams/model/model_competition_team_detail.dart b/lib/features/competition_teams/model/model_competition_team_detail.dart new file mode 100644 index 0000000..44de5bc --- /dev/null +++ b/lib/features/competition_teams/model/model_competition_team_detail.dart @@ -0,0 +1,174 @@ +/// 参赛队伍详情 +class CompetitionTeamDetail { + String? scheduleId; + String? itemId; + String? eventId; + String? itemName; + String? groupName; + String? matchPlace; + String? matchStartTime; + String? matchEndTime; + List? matchups; + + CompetitionTeamDetail({ + this.scheduleId, + this.itemId, + this.eventId, + this.itemName, + this.groupName, + this.matchPlace, + this.matchStartTime, + this.matchEndTime, + this.matchups, + }); + + factory CompetitionTeamDetail.fromJson(Map json) => + CompetitionTeamDetail( + scheduleId: json['scheduleId']?.toString(), + itemId: json['itemId']?.toString(), + eventId: json['eventId']?.toString(), + itemName: json['itemName']?.toString(), + groupName: json['groupName']?.toString(), + matchPlace: json['matchPlace']?.toString(), + matchStartTime: json['matchStartTime']?.toString(), + matchEndTime: json['matchEndTime']?.toString(), + matchups: json['matchups'] == null + ? const [] + : List.from( + (json['matchups'] as List).whereType().map( + (x) => Matchup.fromJson(Map.from(x)), + ), + ), + ); + + /// 兼容 data 为对象或数组(取首项) + factory CompetitionTeamDetail.fromResponse(dynamic json) { + if (json is List) { + if (json.isEmpty) return CompetitionTeamDetail(matchups: const []); + final first = json.first; + if (first is Map) { + return CompetitionTeamDetail.fromJson(Map.from(first)); + } + return CompetitionTeamDetail(matchups: const []); + } + if (json is Map) { + return CompetitionTeamDetail.fromJson(Map.from(json)); + } + return CompetitionTeamDetail(matchups: const []); + } + + Map toJson() => { + 'scheduleId': scheduleId, + 'itemId': itemId, + 'eventId': eventId, + 'itemName': itemName, + 'groupName': groupName, + 'matchPlace': matchPlace, + 'matchStartTime': matchStartTime, + 'matchEndTime': matchEndTime, + 'matchups': matchups == null + ? [] + : List.from(matchups!.map((x) => x.toJson())), + }; +} + +class Matchup { + String? matchTitle; + String? opponentId; + List? teamA; + List? teamB; + + Matchup({this.matchTitle, this.opponentId, this.teamA, this.teamB}); + + factory Matchup.fromJson(Map json) => Matchup( + matchTitle: json['matchTitle']?.toString(), + opponentId: json['opponentId']?.toString(), + teamA: json['teamA'] == null + ? const [] + : List.from( + (json['teamA'] as List).whereType().map( + (x) => Team.fromJson(Map.from(x)), + ), + ), + teamB: json['teamB'] == null + ? const [] + : List.from( + (json['teamB'] as List).whereType().map( + (x) => Team.fromJson(Map.from(x)), + ), + ), + ); + + Map toJson() => { + 'matchTitle': matchTitle, + 'opponentId': opponentId, + 'teamA': teamA == null + ? [] + : List.from(teamA!.map((x) => x.toJson())), + 'teamB': teamB == null + ? [] + : List.from(teamB!.map((x) => x.toJson())), + }; +} + +class Team { + String? teamName; + List? players; + + Team({this.teamName, this.players}); + + String get playerNames => (players ?? const []) + .map((player) => player.name?.trim() ?? '') + .where((name) => name.isNotEmpty) + .join('、'); + + /// 队长 Player.id;无队长则取首个选手 + String? get leaderUserId { + final list = players ?? const []; + for (final player in list) { + final id = player.id?.trim() ?? ''; + if (player.isLeader == true && id.isNotEmpty) return id; + } + if (list.isEmpty) return null; + final firstId = list.first.id?.trim() ?? ''; + return firstId.isEmpty ? null : firstId; + } + + factory Team.fromJson(Map json) => Team( + teamName: json['teamName']?.toString(), + players: json['players'] == null + ? const [] + : List.from( + (json['players'] as List).whereType().map( + (x) => Player.fromJson(Map.from(x)), + ), + ), + ); + + Map toJson() => { + 'teamName': teamName, + 'players': players == null + ? [] + : List.from(players!.map((x) => x.toJson())), + }; +} + +class Player { + String? id; + String? name; + bool? isLeader; + + Player({this.id, this.name, this.isLeader}); + + factory Player.fromJson(Map json) => Player( + id: json['id']?.toString(), + name: json['name']?.toString(), + isLeader: json['isLeader'] as bool?, + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'isLeader': isLeader, + }; +} diff --git a/lib/features/competition_teams/pages/page_competition_team_detail.dart b/lib/features/competition_teams/pages/page_competition_team_detail.dart index ea82861..6381959 100644 --- a/lib/features/competition_teams/pages/page_competition_team_detail.dart +++ b/lib/features/competition_teams/pages/page_competition_team_detail.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:recording_tool/core/network/api_exception.dart'; import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart'; +import 'package:recording_tool/features/competition_teams/model/model_competition_team_detail.dart'; import 'package:recording_tool/features/competition_teams/widgets/widget_manual_winner_dialog.dart'; +import 'package:recording_tool/features/events/request_model/request_model_event.dart'; +import 'package:recording_tool/features/events/server/server_events.dart'; import 'package:recording_tool/shared/widgets/app_empty_view.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart'; @@ -18,8 +22,8 @@ class CompetitionTeamDetailPage extends ConsumerStatefulWidget { class _CompetitionTeamDetailPageState extends ConsumerState { - /// matchupIndex -> winnerTeamId - final Map _winners = {}; + /// matchupIndex -> winner side index(0=teamA,1=teamB) + final Map _winners = {}; CompetitionTeamDetail get _detail => widget.detail; @@ -32,9 +36,9 @@ class _CompetitionTeamDetailPageState List get _matchups => _detail.matchups ?? const []; - CompetitionTeam _toCompetitionTeam(Team team) { + CompetitionTeam _toCompetitionTeam(Team team, {required String fallbackId}) { return CompetitionTeam( - id: team.id?.trim().isNotEmpty == true ? team.id! : 'unknown-team', + id: team.leaderUserId ?? fallbackId, name: team.teamName?.trim().isNotEmpty == true ? team.teamName! : '未命名队伍', players: (team.players ?? const []) .map( @@ -55,11 +59,18 @@ class _CompetitionTeamDetailPageState if (teamA == null || teamA.isEmpty || teamB == null || teamB.isEmpty) { return null; } + final winnerSide = _winners[index]; + final convertedA = _toCompetitionTeam(teamA.first, fallbackId: 'team-a'); + final convertedB = _toCompetitionTeam(teamB.first, fallbackId: 'team-b'); return CompetitionMatchup( id: '${_detail.scheduleId ?? _detail.itemId ?? 'match'}-$index', - teamA: _toCompetitionTeam(teamA.first), - teamB: _toCompetitionTeam(teamB.first), - winnerTeamId: _winners[index], + teamA: convertedA, + teamB: convertedB, + winnerTeamId: winnerSide == 0 + ? convertedA.id + : winnerSide == 1 + ? convertedB.id + : null, ); } @@ -86,28 +97,80 @@ class _CompetitionTeamDetailPageState matchup: matchup, index: index, matchTitle: raw.matchTitle?.trim() ?? '', - onManualProcess: () => _handleManualProcess(matchup, index), + onManualProcess: () => _handleManualProcess(raw, index), ); }, ), ); } - Future _handleManualProcess( - CompetitionMatchup matchup, - int index, - ) async { - final winnerTeamId = await ManualWinnerDialog.show( - context, - matchup: matchup, - ); - if (winnerTeamId == null || !mounted) return; + Team? _sideTeam(Matchup matchup, {required bool isTeamA}) { + final list = isTeamA ? matchup.teamA : matchup.teamB; + if (list == null || list.isEmpty) return null; + return list.first; + } - setState(() => _winners[index] = winnerTeamId); - final winnerName = winnerTeamId == matchup.teamA.id - ? matchup.teamA.name - : matchup.teamB.name; - AppToast.show('已设置$winnerName直接获胜'); + Future _handleManualProcess(Matchup raw, int index) async { + final winnerSide = await ManualWinnerDialog.show( + context, + matchup: raw, + initialWinnerSideIndex: _winners[index], + ); + if (winnerSide == null || !mounted) return; + + final teamA = _sideTeam(raw, isTeamA: true); + final teamB = _sideTeam(raw, isTeamA: false); + final winnerTeam = winnerSide == 0 ? teamA : teamB; + final loserTeam = winnerSide == 0 ? teamB : teamA; + + final winnerLeaderId = winnerTeam?.leaderUserId; + final loserLeaderId = loserTeam?.leaderUserId; + if (winnerLeaderId == null || + winnerLeaderId.isEmpty || + loserLeaderId == null || + loserLeaderId.isEmpty) { + AppToast.show('无法识别双方队长'); + return; + } + + try { + await ref + .read(eventsServerProvider) + .setTeamStatus( + req: SetTeamStatusReq( + scheduleId: _detail.scheduleId ?? '', + opponentId: raw.opponentId ?? '', + teamScore: [ + TeamScore( + userId: winnerLeaderId, + firstHalfScore: 0, + secondHalfScore: 0, + decisiveScore: 0, + ifWithdraw: true, + ), + TeamScore( + userId: loserLeaderId, + firstHalfScore: 0, + secondHalfScore: 0, + decisiveScore: 0, + ifWithdraw: false, + ), + ], + ), + ); + if (!mounted) return; + setState(() => _winners[index] = winnerSide); + final winnerName = winnerTeam?.teamName?.trim().isNotEmpty == true + ? winnerTeam!.teamName! + : '胜方'; + AppToast.show('已设置$winnerName直接获胜'); + } catch (error) { + if (!mounted) return; + final message = error is ApiException && error.message.isNotEmpty + ? error.message + : '设置失败,请重试'; + AppToast.show(message); + } } } diff --git a/lib/features/competition_teams/pages/page_event_team_match.dart b/lib/features/competition_teams/pages/page_event_team_match.dart index 4ec1d0b..69f6f98 100644 --- a/lib/features/competition_teams/pages/page_event_team_match.dart +++ b/lib/features/competition_teams/pages/page_event_team_match.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:recording_tool/app/config/app_config.dart'; import 'package:recording_tool/app/router/app_navigator.dart'; +import 'package:recording_tool/core/network/api_exception.dart'; import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart'; +import 'package:recording_tool/features/competition_teams/model/model_competition_team_detail.dart'; import 'package:recording_tool/features/competition_teams/widgets/widget_manual_winner_dialog.dart'; import 'package:recording_tool/features/events/model/model_event_info.dart'; +import 'package:recording_tool/features/events/request_model/request_model_event.dart'; +import 'package:recording_tool/features/events/server/server_events.dart'; import 'package:recording_tool/shared/widgets/app_button.dart'; import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart'; @@ -12,7 +17,7 @@ import 'package:recording_tool/shared/widgets/app_webview.dart'; typedef TeamQrScanner = Future Function(BuildContext context); -class EventTeamMatchPage extends StatefulWidget { +class EventTeamMatchPage extends ConsumerStatefulWidget { const EventTeamMatchPage({ super.key, required this.playerId, @@ -27,12 +32,14 @@ class EventTeamMatchPage extends StatefulWidget { final EventRegistrationItem item; @override - State createState() => _EventTeamMatchPageState(); + ConsumerState createState() => _EventTeamMatchPageState(); } -class _EventTeamMatchPageState extends State { +class _EventTeamMatchPageState extends ConsumerState { final Set _verifiedUserIds = {}; - String? _winnerTeamId; + + /// 0 = 红队(teamA),1 = 蓝队(teamB) + int? _winnerSideIndex; CompetitionTeamDetail get _detail => widget.detail; @@ -75,23 +82,12 @@ class _EventTeamMatchPageState extends State { List get _blueMembers => _toEventMembers(_blueRawTeam?.players); - CompetitionMatchup get _matchup { - final scheduleId = _detail.scheduleId?.trim() ?? ''; - final itemId = _detail.itemId?.trim() ?? ''; - return CompetitionMatchup( - id: scheduleId.isNotEmpty ? scheduleId : '$itemId-team-match', - teamA: _redTeam, - teamB: _blueTeam, - winnerTeamId: _winnerTeamId, - ); - } - CompetitionTeam _toCompetitionTeam( Team? team, { required String fallbackId, required String fallbackName, }) { - final id = team?.id?.trim() ?? ''; + final id = team?.leaderUserId ?? ''; final name = team?.teamName?.trim() ?? ''; final players = (team?.players ?? const []) .map( @@ -162,16 +158,70 @@ class _EventTeamMatchPageState extends State { } Future _manualProcess() async { - final winnerTeamId = await ManualWinnerDialog.show( + final matchup = _firstMatchup; + if (matchup == null) { + AppToast.show('暂无对阵信息'); + return; + } + + final winnerSide = await ManualWinnerDialog.show( context, - matchup: _matchup, + matchup: matchup, + initialWinnerSideIndex: _winnerSideIndex, ); - if (!mounted || winnerTeamId == null) return; - setState(() => _winnerTeamId = winnerTeamId); - final winnerName = winnerTeamId == _redTeam.id - ? _redTeam.name - : _blueTeam.name; - AppToast.show('已设置$winnerName直接获胜'); + if (!mounted || winnerSide == null) return; + + final winnerTeam = winnerSide == 0 ? _redRawTeam : _blueRawTeam; + final loserTeam = winnerSide == 0 ? _blueRawTeam : _redRawTeam; + + final winnerLeaderId = winnerTeam?.leaderUserId; + final loserLeaderId = loserTeam?.leaderUserId; + if (winnerLeaderId == null || + winnerLeaderId.isEmpty || + loserLeaderId == null || + loserLeaderId.isEmpty) { + AppToast.show('无法识别双方队长'); + return; + } + + try { + await ref + .read(eventsServerProvider) + .setTeamStatus( + req: SetTeamStatusReq( + scheduleId: _detail.scheduleId ?? '', + opponentId: widget.item.opponentId, + teamScore: [ + TeamScore( + userId: winnerLeaderId, + firstHalfScore: 0, + secondHalfScore: 0, + decisiveScore: 0, + ifWithdraw: true, + ), + TeamScore( + userId: loserLeaderId, + firstHalfScore: 0, + secondHalfScore: 0, + decisiveScore: 0, + ifWithdraw: false, + ), + ], + ), + ); + if (!mounted) return; + setState(() => _winnerSideIndex = winnerSide); + final winnerName = winnerTeam?.teamName?.trim().isNotEmpty == true + ? winnerTeam!.teamName! + : (winnerSide == 0 ? _redTeam.name : _blueTeam.name); + AppToast.show('已设置$winnerName直接获胜'); + } catch (error) { + if (!mounted) return; + final message = error is ApiException && error.message.isNotEmpty + ? error.message + : '设置失败,请重试'; + AppToast.show(message); + } } void _startDirectly() { @@ -224,7 +274,7 @@ class _EventTeamMatchPageState extends State { backgroundColor: const Color(0xFFFFEEF0), accentColor: const Color(0xFFE84B5B), verifiedUserIds: _verifiedUserIds, - winner: _winnerTeamId == redTeam.id, + winner: _winnerSideIndex == 0, ), SizedBox(height: 16.h), _TeamCard( @@ -234,7 +284,7 @@ class _EventTeamMatchPageState extends State { backgroundColor: const Color(0xFFEDF5FF), accentColor: const Color(0xFF287FDD), verifiedUserIds: _verifiedUserIds, - winner: _winnerTeamId == blueTeam.id, + winner: _winnerSideIndex == 1, emptyMessage: '暂无蓝队成员数据', ), ], diff --git a/lib/features/competition_teams/server/server_competition_teams.dart b/lib/features/competition_teams/server/server_competition_teams.dart index 43caaff..612980d 100644 --- a/lib/features/competition_teams/server/server_competition_teams.dart +++ b/lib/features/competition_teams/server/server_competition_teams.dart @@ -3,6 +3,7 @@ import 'package:recording_tool/app/config/api_common.dart'; import 'package:recording_tool/core/network/api_client.dart'; import 'package:recording_tool/core/network/providers/dio_providers.dart'; import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart'; +import 'package:recording_tool/features/competition_teams/model/model_competition_team_detail.dart'; final competitionTeamsServerProvider = Provider((ref) { return CompetitionTeamsServer(ref.watch(apiClientProvider)); diff --git a/lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart b/lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart index 1d77e95..be2c4bd 100644 --- a/lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart +++ b/lib/features/competition_teams/widgets/widget_manual_winner_dialog.dart @@ -1,21 +1,31 @@ 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/features/competition_teams/model/model_competition_team_detail.dart'; import 'package:recording_tool/shared/widgets/app_button.dart'; +/// 弹窗回传选中侧索引:0 = teamA(红队),1 = teamB(蓝队) class ManualWinnerDialog extends StatefulWidget { - const ManualWinnerDialog({super.key, required this.matchup}); + const ManualWinnerDialog({ + super.key, + required this.matchup, + this.initialWinnerSideIndex, + }); - final CompetitionMatchup matchup; + final Matchup matchup; + final int? initialWinnerSideIndex; - static Future show( + static Future show( BuildContext context, { - required CompetitionMatchup matchup, + required Matchup matchup, + int? initialWinnerSideIndex, }) { - return showDialog( + return showDialog( context: context, barrierDismissible: false, - builder: (_) => ManualWinnerDialog(matchup: matchup), + builder: (_) => ManualWinnerDialog( + matchup: matchup, + initialWinnerSideIndex: initialWinnerSideIndex, + ), ); } @@ -24,16 +34,35 @@ class ManualWinnerDialog extends StatefulWidget { } class _ManualWinnerDialogState extends State { - String? _selectedTeamId; + /// 0 = teamA,1 = teamB + int? _selectedSideIndex; + + Team? get _teamA { + final teams = widget.matchup.teamA; + if (teams == null || teams.isEmpty) return null; + return teams.first; + } + + Team? get _teamB { + final teams = widget.matchup.teamB; + if (teams == null || teams.isEmpty) return null; + return teams.first; + } @override void initState() { super.initState(); - _selectedTeamId = widget.matchup.winnerTeamId; + final initial = widget.initialWinnerSideIndex; + if (initial == 0 || initial == 1) { + _selectedSideIndex = initial; + } } @override Widget build(BuildContext context) { + final teamA = _teamA; + final teamB = _teamB; + return AlertDialog( insetPadding: EdgeInsets.symmetric(horizontal: 22.w), contentPadding: EdgeInsets.zero, @@ -65,23 +94,24 @@ class _ManualWinnerDialogState extends State { ), ), 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, + if (teamA != null) ...[ + _TeamChoice( + team: teamA, + sideIndex: 0, + color: const Color(0xFFFF6B75), + selected: _selectedSideIndex == 0, + onTap: () => setState(() => _selectedSideIndex = 0), ), - ), - 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: 10.h), + ], + if (teamB != null) + _TeamChoice( + team: teamB, + sideIndex: 1, + color: const Color(0xFF20BFA9), + selected: _selectedSideIndex == 1, + onTap: () => setState(() => _selectedSideIndex = 1), ), - ), SizedBox(height: 20.h), Row( children: [ @@ -96,11 +126,11 @@ class _ManualWinnerDialogState extends State { Expanded( child: AppButton( label: '确定', - onPressed: _selectedTeamId == null + onPressed: _selectedSideIndex == null ? null : () => Navigator.of( context, - ).pop(_selectedTeamId), + ).pop(_selectedSideIndex), ), ), ], @@ -119,27 +149,33 @@ class _ManualWinnerDialogState extends State { class _TeamChoice extends StatelessWidget { const _TeamChoice({ required this.team, + required this.sideIndex, required this.color, required this.selected, required this.onTap, }); - final CompetitionTeam team; + final Team team; + final int sideIndex; final Color color; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { + final name = team.teamName?.trim().isNotEmpty == true + ? team.teamName! + : '未命名队伍'; + final players = team.playerNames; return Semantics( selected: selected, button: true, - label: '选择${team.name}直接获胜', + label: '选择$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}'), + key: ValueKey('winner-choice-side-$sideIndex'), onTap: onTap, borderRadius: BorderRadius.circular(12.r), child: Container( @@ -173,21 +209,23 @@ class _TeamChoice extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - team.name, + 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), + if (players.isNotEmpty) ...[ + SizedBox(height: 4.h), + Text( + players, + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF606874), + ), ), - ), + ], ], ), ), diff --git a/lib/features/events/pages/page_event_info.dart b/lib/features/events/pages/page_event_info.dart index 92ac26d..d8d5540 100644 --- a/lib/features/events/pages/page_event_info.dart +++ b/lib/features/events/pages/page_event_info.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:recording_tool/app/config/app_config.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/model/model_competition_team_detail.dart'; import 'package:recording_tool/features/competition_teams/pages/page_event_team_match.dart'; import 'package:recording_tool/features/competition_teams/server/server_competition_teams.dart'; import 'package:recording_tool/features/events/model/model_event_info.dart'; diff --git a/lib/features/events/request_model/request_model_event.dart b/lib/features/events/request_model/request_model_event.dart new file mode 100644 index 0000000..ba5aa53 --- /dev/null +++ b/lib/features/events/request_model/request_model_event.dart @@ -0,0 +1,58 @@ +class SetTeamStatusReq { + final String scheduleId; + final String opponentId; + final List teamScore; + + SetTeamStatusReq({ + required this.scheduleId, + required this.opponentId, + required this.teamScore, + }); + + factory SetTeamStatusReq.fromJson(Map json) => + SetTeamStatusReq( + scheduleId: json['scheduleId'], + opponentId: json['opponentId'], + teamScore: List.from( + json['teamScore'].map((x) => TeamScore.fromJson(x)), + ), + ); + + Map toJson() => { + 'scheduleId': scheduleId, + 'opponentId': opponentId, + 'teamScore': List.from(teamScore.map((x) => x.toJson())), + }; +} + +class TeamScore { + final String userId; + final int firstHalfScore; + final int secondHalfScore; + final int decisiveScore; + final bool ifWithdraw; + + TeamScore({ + required this.userId, + required this.firstHalfScore, + required this.secondHalfScore, + required this.decisiveScore, + required this.ifWithdraw, + }); + + factory TeamScore.fromJson(Map json) => TeamScore( + userId: json['userId'], + firstHalfScore: json['firstHalfScore'], + secondHalfScore: json['secondHalfScore'], + decisiveScore: json['decisiveScore'], + ifWithdraw: json['ifWithdraw'], + ); + + Map toJson() => { + 'userId': userId, + 'firstHalfScore': firstHalfScore, + 'secondHalfScore': secondHalfScore, + 'decisiveScore': decisiveScore, + 'ifWithdraw': ifWithdraw, + }; +} diff --git a/lib/features/events/server/server_events.dart b/lib/features/events/server/server_events.dart index f7772b1..d6d5d27 100644 --- a/lib/features/events/server/server_events.dart +++ b/lib/features/events/server/server_events.dart @@ -5,6 +5,7 @@ import 'package:recording_tool/core/network/api_client.dart'; import 'package:recording_tool/core/network/http_method.dart'; import 'package:recording_tool/core/network/providers/dio_providers.dart'; import 'package:recording_tool/features/events/model/model_event_info.dart'; +import 'package:recording_tool/features/events/request_model/request_model_event.dart'; final eventsServerProvider = Provider((ref) { return EventsServer(ref.watch(apiClientProvider)); @@ -34,4 +35,14 @@ class EventsServer { parser: StreamKeyResponse.fromJson, ); } + + /// 人工设置晋级/淘汰 + Future setTeamStatus({required SetTeamStatusReq req}) { + print('req: ${req.toJson()}'); + return _apiClient.post( + AuthApi.setTeamStatus.path, + data: req.toJson(), + parser: (json) => json, + ); + } }