新增人工设置晋级/淘汰功能,更新相关数据模型和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
+3
View File
@@ -14,6 +14,9 @@ enum AuthApi {
/// 参赛队伍详情 /// 参赛队伍详情
getTeamDetail('/api/events/device/schedule/pending'), getTeamDetail('/api/events/device/schedule/pending'),
/// 人工设置晋级/淘汰
setTeamStatus('/api/events/device/schedule/team/score'),
/// 获取选手的赛事信息 /// 获取选手的赛事信息
playerRegistrationList('/api/events/device/player/registration/list'); playerRegistrationList('/api/events/device/player/registration/list');
@@ -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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.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.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/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_empty_view.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart';
@@ -18,8 +22,8 @@ class CompetitionTeamDetailPage extends ConsumerStatefulWidget {
class _CompetitionTeamDetailPageState class _CompetitionTeamDetailPageState
extends ConsumerState<CompetitionTeamDetailPage> { extends ConsumerState<CompetitionTeamDetailPage> {
/// matchupIndex -> winnerTeamId /// matchupIndex -> winner side index0=teamA1=teamB
final Map<int, String> _winners = {}; final Map<int, int> _winners = {};
CompetitionTeamDetail get _detail => widget.detail; CompetitionTeamDetail get _detail => widget.detail;
@@ -32,9 +36,9 @@ class _CompetitionTeamDetailPageState
List<Matchup> get _matchups => _detail.matchups ?? const <Matchup>[]; List<Matchup> get _matchups => _detail.matchups ?? const <Matchup>[];
CompetitionTeam _toCompetitionTeam(Team team) { CompetitionTeam _toCompetitionTeam(Team team, {required String fallbackId}) {
return CompetitionTeam( return CompetitionTeam(
id: team.id?.trim().isNotEmpty == true ? team.id! : 'unknown-team', id: team.leaderUserId ?? fallbackId,
name: team.teamName?.trim().isNotEmpty == true ? team.teamName! : '未命名队伍', name: team.teamName?.trim().isNotEmpty == true ? team.teamName! : '未命名队伍',
players: (team.players ?? const <Player>[]) players: (team.players ?? const <Player>[])
.map( .map(
@@ -55,11 +59,18 @@ class _CompetitionTeamDetailPageState
if (teamA == null || teamA.isEmpty || teamB == null || teamB.isEmpty) { if (teamA == null || teamA.isEmpty || teamB == null || teamB.isEmpty) {
return null; return null;
} }
final winnerSide = _winners[index];
final convertedA = _toCompetitionTeam(teamA.first, fallbackId: 'team-a');
final convertedB = _toCompetitionTeam(teamB.first, fallbackId: 'team-b');
return CompetitionMatchup( return CompetitionMatchup(
id: '${_detail.scheduleId ?? _detail.itemId ?? 'match'}-$index', id: '${_detail.scheduleId ?? _detail.itemId ?? 'match'}-$index',
teamA: _toCompetitionTeam(teamA.first), teamA: convertedA,
teamB: _toCompetitionTeam(teamB.first), teamB: convertedB,
winnerTeamId: _winners[index], winnerTeamId: winnerSide == 0
? convertedA.id
: winnerSide == 1
? convertedB.id
: null,
); );
} }
@@ -86,28 +97,80 @@ class _CompetitionTeamDetailPageState
matchup: matchup, matchup: matchup,
index: index, index: index,
matchTitle: raw.matchTitle?.trim() ?? '', matchTitle: raw.matchTitle?.trim() ?? '',
onManualProcess: () => _handleManualProcess(matchup, index), onManualProcess: () => _handleManualProcess(raw, index),
); );
}, },
), ),
); );
} }
Future<void> _handleManualProcess( Team? _sideTeam(Matchup matchup, {required bool isTeamA}) {
CompetitionMatchup matchup, final list = isTeamA ? matchup.teamA : matchup.teamB;
int index, if (list == null || list.isEmpty) return null;
) async { return list.first;
final winnerTeamId = await ManualWinnerDialog.show( }
context,
matchup: matchup,
);
if (winnerTeamId == null || !mounted) return;
setState(() => _winners[index] = winnerTeamId); Future<void> _handleManualProcess(Matchup raw, int index) async {
final winnerName = winnerTeamId == matchup.teamA.id final winnerSide = await ManualWinnerDialog.show(
? matchup.teamA.name context,
: matchup.teamB.name; 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直接获胜'); 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/app/config/app_config.dart'; import 'package:recording_tool/app/config/app_config.dart';
import 'package:recording_tool/app/router/app_navigator.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.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/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/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_button.dart';
import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart'; import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart';
import 'package:recording_tool/shared/widgets/app_toast.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); typedef TeamQrScanner = Future<String?> Function(BuildContext context);
class EventTeamMatchPage extends StatefulWidget { class EventTeamMatchPage extends ConsumerStatefulWidget {
const EventTeamMatchPage({ const EventTeamMatchPage({
super.key, super.key,
required this.playerId, required this.playerId,
@@ -27,12 +32,14 @@ class EventTeamMatchPage extends StatefulWidget {
final EventRegistrationItem item; final EventRegistrationItem item;
@override @override
State<EventTeamMatchPage> createState() => _EventTeamMatchPageState(); ConsumerState<EventTeamMatchPage> createState() => _EventTeamMatchPageState();
} }
class _EventTeamMatchPageState extends State<EventTeamMatchPage> { class _EventTeamMatchPageState extends ConsumerState<EventTeamMatchPage> {
final Set<String> _verifiedUserIds = <String>{}; final Set<String> _verifiedUserIds = <String>{};
String? _winnerTeamId;
/// 0 = 红队(teamA)1 = 蓝队(teamB)
int? _winnerSideIndex;
CompetitionTeamDetail get _detail => widget.detail; CompetitionTeamDetail get _detail => widget.detail;
@@ -75,23 +82,12 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
List<EventTeamMember> get _blueMembers => List<EventTeamMember> get _blueMembers =>
_toEventMembers(_blueRawTeam?.players); _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( CompetitionTeam _toCompetitionTeam(
Team? team, { Team? team, {
required String fallbackId, required String fallbackId,
required String fallbackName, required String fallbackName,
}) { }) {
final id = team?.id?.trim() ?? ''; final id = team?.leaderUserId ?? '';
final name = team?.teamName?.trim() ?? ''; final name = team?.teamName?.trim() ?? '';
final players = (team?.players ?? const <Player>[]) final players = (team?.players ?? const <Player>[])
.map( .map(
@@ -162,16 +158,70 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
} }
Future<void> _manualProcess() async { Future<void> _manualProcess() async {
final winnerTeamId = await ManualWinnerDialog.show( final matchup = _firstMatchup;
if (matchup == null) {
AppToast.show('暂无对阵信息');
return;
}
final winnerSide = await ManualWinnerDialog.show(
context, context,
matchup: _matchup, matchup: matchup,
initialWinnerSideIndex: _winnerSideIndex,
); );
if (!mounted || winnerTeamId == null) return; if (!mounted || winnerSide == null) return;
setState(() => _winnerTeamId = winnerTeamId);
final winnerName = winnerTeamId == _redTeam.id final winnerTeam = winnerSide == 0 ? _redRawTeam : _blueRawTeam;
? _redTeam.name final loserTeam = winnerSide == 0 ? _blueRawTeam : _redRawTeam;
: _blueTeam.name;
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直接获胜'); AppToast.show('已设置$winnerName直接获胜');
} catch (error) {
if (!mounted) return;
final message = error is ApiException && error.message.isNotEmpty
? error.message
: '设置失败,请重试';
AppToast.show(message);
}
} }
void _startDirectly() { void _startDirectly() {
@@ -224,7 +274,7 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
backgroundColor: const Color(0xFFFFEEF0), backgroundColor: const Color(0xFFFFEEF0),
accentColor: const Color(0xFFE84B5B), accentColor: const Color(0xFFE84B5B),
verifiedUserIds: _verifiedUserIds, verifiedUserIds: _verifiedUserIds,
winner: _winnerTeamId == redTeam.id, winner: _winnerSideIndex == 0,
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
_TeamCard( _TeamCard(
@@ -234,7 +284,7 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
backgroundColor: const Color(0xFFEDF5FF), backgroundColor: const Color(0xFFEDF5FF),
accentColor: const Color(0xFF287FDD), accentColor: const Color(0xFF287FDD),
verifiedUserIds: _verifiedUserIds, verifiedUserIds: _verifiedUserIds,
winner: _winnerTeamId == blueTeam.id, winner: _winnerSideIndex == 1,
emptyMessage: '暂无蓝队成员数据', 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/api_client.dart';
import 'package:recording_tool/core/network/providers/dio_providers.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.dart';
import 'package:recording_tool/features/competition_teams/model/model_competition_team_detail.dart';
final competitionTeamsServerProvider = Provider<CompetitionTeamsServer>((ref) { final competitionTeamsServerProvider = Provider<CompetitionTeamsServer>((ref) {
return CompetitionTeamsServer(ref.watch(apiClientProvider)); return CompetitionTeamsServer(ref.watch(apiClientProvider));
@@ -1,21 +1,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.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'; import 'package:recording_tool/shared/widgets/app_button.dart';
/// 弹窗回传选中侧索引:0 = teamA(红队),1 = teamB(蓝队)
class ManualWinnerDialog extends StatefulWidget { 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, { BuildContext context, {
required CompetitionMatchup matchup, required Matchup matchup,
int? initialWinnerSideIndex,
}) { }) {
return showDialog<String>( return showDialog<int>(
context: context, context: context,
barrierDismissible: false, 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> { class _ManualWinnerDialogState extends State<ManualWinnerDialog> {
String? _selectedTeamId; /// 0 = teamA1 = 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 @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedTeamId = widget.matchup.winnerTeamId; final initial = widget.initialWinnerSideIndex;
if (initial == 0 || initial == 1) {
_selectedSideIndex = initial;
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final teamA = _teamA;
final teamB = _teamB;
return AlertDialog( return AlertDialog(
insetPadding: EdgeInsets.symmetric(horizontal: 22.w), insetPadding: EdgeInsets.symmetric(horizontal: 22.w),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@@ -65,22 +94,23 @@ class _ManualWinnerDialogState extends State<ManualWinnerDialog> {
), ),
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
if (teamA != null) ...[
_TeamChoice( _TeamChoice(
team: widget.matchup.teamA, team: teamA,
sideIndex: 0,
color: const Color(0xFFFF6B75), color: const Color(0xFFFF6B75),
selected: _selectedTeamId == widget.matchup.teamA.id, selected: _selectedSideIndex == 0,
onTap: () => setState( onTap: () => setState(() => _selectedSideIndex = 0),
() => _selectedTeamId = widget.matchup.teamA.id,
),
), ),
SizedBox(height: 10.h), SizedBox(height: 10.h),
],
if (teamB != null)
_TeamChoice( _TeamChoice(
team: widget.matchup.teamB, team: teamB,
sideIndex: 1,
color: const Color(0xFF20BFA9), color: const Color(0xFF20BFA9),
selected: _selectedTeamId == widget.matchup.teamB.id, selected: _selectedSideIndex == 1,
onTap: () => setState( onTap: () => setState(() => _selectedSideIndex = 1),
() => _selectedTeamId = widget.matchup.teamB.id,
),
), ),
SizedBox(height: 20.h), SizedBox(height: 20.h),
Row( Row(
@@ -96,11 +126,11 @@ class _ManualWinnerDialogState extends State<ManualWinnerDialog> {
Expanded( Expanded(
child: AppButton( child: AppButton(
label: '确定', label: '确定',
onPressed: _selectedTeamId == null onPressed: _selectedSideIndex == null
? null ? null
: () => Navigator.of( : () => Navigator.of(
context, context,
).pop(_selectedTeamId), ).pop(_selectedSideIndex),
), ),
), ),
], ],
@@ -119,27 +149,33 @@ class _ManualWinnerDialogState extends State<ManualWinnerDialog> {
class _TeamChoice extends StatelessWidget { class _TeamChoice extends StatelessWidget {
const _TeamChoice({ const _TeamChoice({
required this.team, required this.team,
required this.sideIndex,
required this.color, required this.color,
required this.selected, required this.selected,
required this.onTap, required this.onTap,
}); });
final CompetitionTeam team; final Team team;
final int sideIndex;
final Color color; final Color color;
final bool selected; final bool selected;
final VoidCallback onTap; final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final name = team.teamName?.trim().isNotEmpty == true
? team.teamName!
: '未命名队伍';
final players = team.playerNames;
return Semantics( return Semantics(
selected: selected, selected: selected,
button: true, button: true,
label: '选择${team.name}直接获胜', label: '选择$name直接获胜',
child: Material( child: Material(
color: color.withValues(alpha: selected ? 0.16 : 0.08), color: color.withValues(alpha: selected ? 0.16 : 0.08),
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
child: InkWell( child: InkWell(
key: ValueKey('winner-choice-${team.id}'), key: ValueKey('winner-choice-side-$sideIndex'),
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
child: Container( child: Container(
@@ -173,22 +209,24 @@ class _TeamChoice extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
team.name, name,
style: TextStyle( style: TextStyle(
fontSize: 17.sp, fontSize: 17.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: const Color(0xFF20242B), color: const Color(0xFF20242B),
), ),
), ),
if (players.isNotEmpty) ...[
SizedBox(height: 4.h), SizedBox(height: 4.h),
Text( Text(
team.playerNames, players,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: const Color(0xFF606874), color: const Color(0xFF606874),
), ),
), ),
], ],
],
), ),
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:recording_tool/app/config/app_config.dart'; import 'package:recording_tool/app/config/app_config.dart';
import 'package:recording_tool/app/router/app_navigator.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/pages/page_event_team_match.dart';
import 'package:recording_tool/features/competition_teams/server/server_competition_teams.dart'; import 'package:recording_tool/features/competition_teams/server/server_competition_teams.dart';
import 'package:recording_tool/features/events/model/model_event_info.dart'; import 'package:recording_tool/features/events/model/model_event_info.dart';
@@ -0,0 +1,58 @@
class SetTeamStatusReq {
final String scheduleId;
final String opponentId;
final List<TeamScore> teamScore;
SetTeamStatusReq({
required this.scheduleId,
required this.opponentId,
required this.teamScore,
});
factory SetTeamStatusReq.fromJson(Map<String, dynamic> json) =>
SetTeamStatusReq(
scheduleId: json['scheduleId'],
opponentId: json['opponentId'],
teamScore: List<TeamScore>.from(
json['teamScore'].map((x) => TeamScore.fromJson(x)),
),
);
Map<String, dynamic> toJson() => {
'scheduleId': scheduleId,
'opponentId': opponentId,
'teamScore': List<dynamic>.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<String, dynamic> json) => TeamScore(
userId: json['userId'],
firstHalfScore: json['firstHalfScore'],
secondHalfScore: json['secondHalfScore'],
decisiveScore: json['decisiveScore'],
ifWithdraw: json['ifWithdraw'],
);
Map<String, dynamic> toJson() => {
'userId': userId,
'firstHalfScore': firstHalfScore,
'secondHalfScore': secondHalfScore,
'decisiveScore': decisiveScore,
'ifWithdraw': ifWithdraw,
};
}
@@ -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/http_method.dart';
import 'package:recording_tool/core/network/providers/dio_providers.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/model/model_event_info.dart';
import 'package:recording_tool/features/events/request_model/request_model_event.dart';
final eventsServerProvider = Provider<EventsServer>((ref) { final eventsServerProvider = Provider<EventsServer>((ref) {
return EventsServer(ref.watch(apiClientProvider)); return EventsServer(ref.watch(apiClientProvider));
@@ -34,4 +35,14 @@ class EventsServer {
parser: StreamKeyResponse.fromJson, parser: StreamKeyResponse.fromJson,
); );
} }
/// 人工设置晋级/淘汰
Future<void> setTeamStatus({required SetTeamStatusReq req}) {
print('req: ${req.toJson()}');
return _apiClient.post(
AuthApi.setTeamStatus.path,
data: req.toJson(),
parser: (json) => json,
);
}
} }