新增人工设置晋级/淘汰功能,更新相关数据模型和API接口;重构比赛页面以支持新逻辑,优化用户交互体验。
This commit is contained in:
@@ -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<Matchup>? matchups;
|
||||
|
||||
CompetitionTeamDetail({
|
||||
this.scheduleId,
|
||||
this.itemId,
|
||||
this.eventId,
|
||||
this.itemName,
|
||||
this.groupName,
|
||||
this.matchPlace,
|
||||
this.matchStartTime,
|
||||
this.matchEndTime,
|
||||
this.matchups,
|
||||
});
|
||||
|
||||
factory CompetitionTeamDetail.fromJson(Map<String, dynamic> 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<Matchup>.from(
|
||||
(json['matchups'] as List).whereType<Map>().map(
|
||||
(x) => Matchup.fromJson(Map<String, dynamic>.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<String, dynamic>.from(first));
|
||||
}
|
||||
return CompetitionTeamDetail(matchups: const []);
|
||||
}
|
||||
if (json is Map) {
|
||||
return CompetitionTeamDetail.fromJson(Map<String, dynamic>.from(json));
|
||||
}
|
||||
return CompetitionTeamDetail(matchups: const []);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'scheduleId': scheduleId,
|
||||
'itemId': itemId,
|
||||
'eventId': eventId,
|
||||
'itemName': itemName,
|
||||
'groupName': groupName,
|
||||
'matchPlace': matchPlace,
|
||||
'matchStartTime': matchStartTime,
|
||||
'matchEndTime': matchEndTime,
|
||||
'matchups': matchups == null
|
||||
? []
|
||||
: List<dynamic>.from(matchups!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class Matchup {
|
||||
String? matchTitle;
|
||||
List<Team>? teamA;
|
||||
List<Team>? teamB;
|
||||
|
||||
Matchup({this.matchTitle, this.teamA, this.teamB});
|
||||
|
||||
factory Matchup.fromJson(Map<String, dynamic> json) => Matchup(
|
||||
matchTitle: json['matchTitle']?.toString(),
|
||||
teamA: json['teamA'] == null
|
||||
? const []
|
||||
: List<Team>.from(
|
||||
(json['teamA'] as List).whereType<Map>().map(
|
||||
(x) => Team.fromJson(Map<String, dynamic>.from(x)),
|
||||
),
|
||||
),
|
||||
teamB: json['teamB'] == null
|
||||
? const []
|
||||
: List<Team>.from(
|
||||
(json['teamB'] as List).whereType<Map>().map(
|
||||
(x) => Team.fromJson(Map<String, dynamic>.from(x)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'matchTitle': matchTitle,
|
||||
'teamA': teamA == null
|
||||
? []
|
||||
: List<dynamic>.from(teamA!.map((x) => x.toJson())),
|
||||
'teamB': teamB == null
|
||||
? []
|
||||
: List<dynamic>.from(teamB!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class Team {
|
||||
String? id;
|
||||
String? teamName;
|
||||
List<Player>? players;
|
||||
|
||||
Team({this.id, this.teamName, this.players});
|
||||
|
||||
factory Team.fromJson(Map<String, dynamic> json) => Team(
|
||||
id: json['id']?.toString(),
|
||||
teamName: json['teamName']?.toString(),
|
||||
players: json['players'] == null
|
||||
? const []
|
||||
: List<Player>.from(
|
||||
(json['players'] as List).whereType<Map>().map(
|
||||
(x) => Player.fromJson(Map<String, dynamic>.from(x)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'teamName': teamName,
|
||||
'players': players == null
|
||||
? []
|
||||
: List<dynamic>.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<String, dynamic> json) => Player(
|
||||
id: json['id']?.toString(),
|
||||
name: json['name']?.toString(),
|
||||
isLeader: json['isLeader'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'isLeader': isLeader,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/// 参赛队伍详情
|
||||
class CompetitionTeamDetail {
|
||||
String? scheduleId;
|
||||
String? itemId;
|
||||
String? eventId;
|
||||
String? itemName;
|
||||
String? groupName;
|
||||
String? matchPlace;
|
||||
String? matchStartTime;
|
||||
String? matchEndTime;
|
||||
List<Matchup>? matchups;
|
||||
|
||||
CompetitionTeamDetail({
|
||||
this.scheduleId,
|
||||
this.itemId,
|
||||
this.eventId,
|
||||
this.itemName,
|
||||
this.groupName,
|
||||
this.matchPlace,
|
||||
this.matchStartTime,
|
||||
this.matchEndTime,
|
||||
this.matchups,
|
||||
});
|
||||
|
||||
factory CompetitionTeamDetail.fromJson(Map<String, dynamic> 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<Matchup>.from(
|
||||
(json['matchups'] as List).whereType<Map>().map(
|
||||
(x) => Matchup.fromJson(Map<String, dynamic>.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<String, dynamic>.from(first));
|
||||
}
|
||||
return CompetitionTeamDetail(matchups: const []);
|
||||
}
|
||||
if (json is Map) {
|
||||
return CompetitionTeamDetail.fromJson(Map<String, dynamic>.from(json));
|
||||
}
|
||||
return CompetitionTeamDetail(matchups: const []);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'scheduleId': scheduleId,
|
||||
'itemId': itemId,
|
||||
'eventId': eventId,
|
||||
'itemName': itemName,
|
||||
'groupName': groupName,
|
||||
'matchPlace': matchPlace,
|
||||
'matchStartTime': matchStartTime,
|
||||
'matchEndTime': matchEndTime,
|
||||
'matchups': matchups == null
|
||||
? []
|
||||
: List<dynamic>.from(matchups!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class Matchup {
|
||||
String? matchTitle;
|
||||
String? opponentId;
|
||||
List<Team>? teamA;
|
||||
List<Team>? teamB;
|
||||
|
||||
Matchup({this.matchTitle, this.opponentId, this.teamA, this.teamB});
|
||||
|
||||
factory Matchup.fromJson(Map<String, dynamic> json) => Matchup(
|
||||
matchTitle: json['matchTitle']?.toString(),
|
||||
opponentId: json['opponentId']?.toString(),
|
||||
teamA: json['teamA'] == null
|
||||
? const []
|
||||
: List<Team>.from(
|
||||
(json['teamA'] as List).whereType<Map>().map(
|
||||
(x) => Team.fromJson(Map<String, dynamic>.from(x)),
|
||||
),
|
||||
),
|
||||
teamB: json['teamB'] == null
|
||||
? const []
|
||||
: List<Team>.from(
|
||||
(json['teamB'] as List).whereType<Map>().map(
|
||||
(x) => Team.fromJson(Map<String, dynamic>.from(x)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'matchTitle': matchTitle,
|
||||
'opponentId': opponentId,
|
||||
'teamA': teamA == null
|
||||
? []
|
||||
: List<dynamic>.from(teamA!.map((x) => x.toJson())),
|
||||
'teamB': teamB == null
|
||||
? []
|
||||
: List<dynamic>.from(teamB!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class Team {
|
||||
String? teamName;
|
||||
List<Player>? players;
|
||||
|
||||
Team({this.teamName, this.players});
|
||||
|
||||
String get playerNames => (players ?? const <Player>[])
|
||||
.map((player) => player.name?.trim() ?? '')
|
||||
.where((name) => name.isNotEmpty)
|
||||
.join('、');
|
||||
|
||||
/// 队长 Player.id;无队长则取首个选手
|
||||
String? get leaderUserId {
|
||||
final list = players ?? const <Player>[];
|
||||
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<String, dynamic> json) => Team(
|
||||
teamName: json['teamName']?.toString(),
|
||||
players: json['players'] == null
|
||||
? const []
|
||||
: List<Player>.from(
|
||||
(json['players'] as List).whereType<Map>().map(
|
||||
(x) => Player.fromJson(Map<String, dynamic>.from(x)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'teamName': teamName,
|
||||
'players': players == null
|
||||
? []
|
||||
: List<dynamic>.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<String, dynamic> json) => Player(
|
||||
id: json['id']?.toString(),
|
||||
name: json['name']?.toString(),
|
||||
isLeader: json['isLeader'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'isLeader': isLeader,
|
||||
};
|
||||
}
|
||||
@@ -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 index(0=teamA,1=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: '暂无蓝队成员数据',
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<CompetitionTeamsServer>((ref) {
|
||||
return CompetitionTeamsServer(ref.watch(apiClientProvider));
|
||||
|
||||
@@ -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<String?> show(
|
||||
static Future<int?> show(
|
||||
BuildContext context, {
|
||||
required CompetitionMatchup matchup,
|
||||
required Matchup matchup,
|
||||
int? initialWinnerSideIndex,
|
||||
}) {
|
||||
return showDialog<String>(
|
||||
return showDialog<int>(
|
||||
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<ManualWinnerDialog> {
|
||||
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<ManualWinnerDialog> {
|
||||
),
|
||||
),
|
||||
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<ManualWinnerDialog> {
|
||||
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<ManualWinnerDialog> {
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user