新增人工设置晋级/淘汰功能,更新相关数据模型和API接口;重构比赛页面以支持新逻辑,优化用户交互体验。

This commit is contained in:
2026-07-23 15:35:31 +08:00
parent 00937bf4b6
commit 10d9eee60e
10 changed files with 484 additions and 244 deletions
@@ -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<CompetitionTeamDetailPage> {
/// matchupIndex -> winnerTeamId
final Map<int, String> _winners = {};
/// matchupIndex -> winner side index0=teamA1=teamB
final Map<int, int> _winners = {};
CompetitionTeamDetail get _detail => widget.detail;
@@ -32,9 +36,9 @@ class _CompetitionTeamDetailPageState
List<Matchup> get _matchups => _detail.matchups ?? const <Matchup>[];
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 <Player>[])
.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<void> _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<void> _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);
}
}
}
@@ -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<String?> 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<EventTeamMatchPage> createState() => _EventTeamMatchPageState();
ConsumerState<EventTeamMatchPage> createState() => _EventTeamMatchPageState();
}
class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
class _EventTeamMatchPageState extends ConsumerState<EventTeamMatchPage> {
final Set<String> _verifiedUserIds = <String>{};
String? _winnerTeamId;
/// 0 = 红队(teamA)1 = 蓝队(teamB)
int? _winnerSideIndex;
CompetitionTeamDetail get _detail => widget.detail;
@@ -75,23 +82,12 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
List<EventTeamMember> 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 <Player>[])
.map(
@@ -162,16 +158,70 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
}
Future<void> _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<EventTeamMatchPage> {
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<EventTeamMatchPage> {
backgroundColor: const Color(0xFFEDF5FF),
accentColor: const Color(0xFF287FDD),
verifiedUserIds: _verifiedUserIds,
winner: _winnerTeamId == blueTeam.id,
winner: _winnerSideIndex == 1,
emptyMessage: '暂无蓝队成员数据',
),
],