1.重构参赛队伍详情页面,更新数据模型以支持新结构;
2.优化比赛信息展示逻辑,增强用户交互体验; 3.修复API请求以获取队伍详情并处理异常情况。
This commit is contained in:
@@ -12,7 +12,7 @@ enum AuthApi {
|
|||||||
getTeamList('/api/events/device/item/group'),
|
getTeamList('/api/events/device/item/group'),
|
||||||
|
|
||||||
/// 参赛队伍详情
|
/// 参赛队伍详情
|
||||||
getTeamDetail('/api/events/device/schedule/pending/score'),
|
getTeamDetail('/api/events/device/schedule/pending'),
|
||||||
|
|
||||||
/// 获取选手的赛事信息
|
/// 获取选手的赛事信息
|
||||||
playerRegistrationList('/api/events/device/player/registration/list');
|
playerRegistrationList('/api/events/device/player/registration/list');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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/app/router/app_navigator.dart';
|
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||||
@@ -23,7 +24,7 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = TextEditingController();
|
_controller = TextEditingController();
|
||||||
// _controller?.text = '299689';
|
_controller?.text = '986570';
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final token = AppStorage.getString(StorageKeys.authToken);
|
final token = AppStorage.getString(StorageKeys.authToken);
|
||||||
@@ -59,7 +60,15 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
AppTextField(controller: _controller),
|
AppTextField(
|
||||||
|
controller: _controller,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 6,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
|
LengthLimitingTextInputFormatter(6),
|
||||||
|
],
|
||||||
|
),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
|
|||||||
@@ -169,3 +169,161 @@ 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,56 +2,91 @@ 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/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/view_model/view_model_competition_teams.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/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';
|
||||||
|
|
||||||
class CompetitionTeamDetailPage extends ConsumerWidget {
|
class CompetitionTeamDetailPage extends ConsumerStatefulWidget {
|
||||||
const CompetitionTeamDetailPage({
|
const CompetitionTeamDetailPage({super.key, required this.detail});
|
||||||
super.key,
|
|
||||||
required this.itemId,
|
|
||||||
required this.eventId,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String itemId;
|
final CompetitionTeamDetail detail;
|
||||||
final String eventId;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<CompetitionTeamDetailPage> createState() =>
|
||||||
final items = ref.watch(
|
_CompetitionTeamDetailPageState();
|
||||||
competitionTeamsProvider.select((state) => state.items),
|
}
|
||||||
|
|
||||||
|
class _CompetitionTeamDetailPageState
|
||||||
|
extends ConsumerState<CompetitionTeamDetailPage> {
|
||||||
|
/// matchupIndex -> winnerTeamId
|
||||||
|
final Map<int, String> _winners = {};
|
||||||
|
|
||||||
|
CompetitionTeamDetail get _detail => widget.detail;
|
||||||
|
|
||||||
|
String get _title {
|
||||||
|
final name = _detail.itemName?.trim() ?? '';
|
||||||
|
final group = _detail.groupName?.trim() ?? '';
|
||||||
|
if (name.isEmpty) return '参赛队伍';
|
||||||
|
return group.isEmpty ? name : '$name ($group)';
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Matchup> get _matchups => _detail.matchups ?? const <Matchup>[];
|
||||||
|
|
||||||
|
CompetitionTeam _toCompetitionTeam(Team team) {
|
||||||
|
return CompetitionTeam(
|
||||||
|
id: team.id?.trim().isNotEmpty == true ? team.id! : 'unknown-team',
|
||||||
|
name: team.teamName?.trim().isNotEmpty == true ? team.teamName! : '未命名队伍',
|
||||||
|
players: (team.players ?? const <Player>[])
|
||||||
|
.map(
|
||||||
|
(player) => CompetitionPlayer(
|
||||||
|
id: player.id?.trim() ?? '',
|
||||||
|
name: player.name?.trim().isNotEmpty == true
|
||||||
|
? player.name!
|
||||||
|
: '选手',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false),
|
||||||
);
|
);
|
||||||
CompetitionTeamListItem? item;
|
}
|
||||||
for (final candidate in items) {
|
|
||||||
if (candidate.itemId == itemId) {
|
CompetitionMatchup? _toCompetitionMatchup(Matchup matchup, int index) {
|
||||||
item = candidate;
|
final teamA = matchup.teamA;
|
||||||
break;
|
final teamB = matchup.teamB;
|
||||||
}
|
if (teamA == null || teamA.isEmpty || teamB == null || teamB.isEmpty) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
return CompetitionMatchup(
|
||||||
|
id: '${_detail.scheduleId ?? _detail.itemId ?? 'match'}-$index',
|
||||||
|
teamA: _toCompetitionTeam(teamA.first),
|
||||||
|
teamB: _toCompetitionTeam(teamB.first),
|
||||||
|
winnerTeamId: _winners[index],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final matchups = _matchups;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(title: Text(item?.title ?? '参赛队伍')),
|
appBar: AppBar(title: Text(_title)),
|
||||||
body: item == null
|
body: matchups.isEmpty
|
||||||
? const AppEmptyView(message: '未找到对阵信息')
|
|
||||||
: item.matchups.isEmpty
|
|
||||||
? const AppEmptyView(message: '暂无对阵信息')
|
? const AppEmptyView(message: '暂无对阵信息')
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
padding: EdgeInsets.fromLTRB(20.w, 24.h, 20.w, 36.h),
|
padding: EdgeInsets.fromLTRB(20.w, 24.h, 20.w, 36.h),
|
||||||
itemCount: item.matchups.length,
|
itemCount: matchups.length,
|
||||||
separatorBuilder: (_, _) => SizedBox(height: 22.h),
|
separatorBuilder: (_, _) => SizedBox(height: 22.h),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final matchup = item!.matchups[index];
|
final raw = matchups[index];
|
||||||
|
final matchup = _toCompetitionMatchup(raw, index);
|
||||||
|
if (matchup == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
return _MatchupCard(
|
return _MatchupCard(
|
||||||
matchup: matchup,
|
matchup: matchup,
|
||||||
index: index,
|
index: index,
|
||||||
onManualProcess: () => _handleManualProcess(
|
matchTitle: raw.matchTitle?.trim() ?? '',
|
||||||
context,
|
onManualProcess: () => _handleManualProcess(matchup, index),
|
||||||
ref,
|
|
||||||
itemId: item!.itemId,
|
|
||||||
matchup: matchup,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -59,24 +94,16 @@ class CompetitionTeamDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleManualProcess(
|
Future<void> _handleManualProcess(
|
||||||
BuildContext context,
|
CompetitionMatchup matchup,
|
||||||
WidgetRef ref, {
|
int index,
|
||||||
required String itemId,
|
) async {
|
||||||
required CompetitionMatchup matchup,
|
|
||||||
}) async {
|
|
||||||
final winnerTeamId = await ManualWinnerDialog.show(
|
final winnerTeamId = await ManualWinnerDialog.show(
|
||||||
context,
|
context,
|
||||||
matchup: matchup,
|
matchup: matchup,
|
||||||
);
|
);
|
||||||
if (winnerTeamId == null || !context.mounted) return;
|
if (winnerTeamId == null || !mounted) return;
|
||||||
|
|
||||||
ref
|
setState(() => _winners[index] = winnerTeamId);
|
||||||
.read(competitionTeamsProvider.notifier)
|
|
||||||
.selectWinner(
|
|
||||||
itemId: itemId,
|
|
||||||
matchupId: matchup.id,
|
|
||||||
winnerTeamId: winnerTeamId,
|
|
||||||
);
|
|
||||||
final winnerName = winnerTeamId == matchup.teamA.id
|
final winnerName = winnerTeamId == matchup.teamA.id
|
||||||
? matchup.teamA.name
|
? matchup.teamA.name
|
||||||
: matchup.teamB.name;
|
: matchup.teamB.name;
|
||||||
@@ -88,15 +115,18 @@ class _MatchupCard extends StatelessWidget {
|
|||||||
const _MatchupCard({
|
const _MatchupCard({
|
||||||
required this.matchup,
|
required this.matchup,
|
||||||
required this.index,
|
required this.index,
|
||||||
|
required this.matchTitle,
|
||||||
required this.onManualProcess,
|
required this.onManualProcess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final CompetitionMatchup matchup;
|
final CompetitionMatchup matchup;
|
||||||
final int index;
|
final int index;
|
||||||
|
final String matchTitle;
|
||||||
final VoidCallback onManualProcess;
|
final VoidCallback onManualProcess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final title = matchTitle.isEmpty ? '第 ${index + 1} 场' : matchTitle;
|
||||||
return Container(
|
return Container(
|
||||||
key: ValueKey('competition-matchup-${matchup.id}'),
|
key: ValueKey('competition-matchup-${matchup.id}'),
|
||||||
padding: EdgeInsets.fromLTRB(16.w, 10.h, 16.w, 18.h),
|
padding: EdgeInsets.fromLTRB(16.w, 10.h, 16.w, 18.h),
|
||||||
@@ -109,14 +139,17 @@ class _MatchupCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
'第 ${index + 1} 场',
|
child: Text(
|
||||||
style: TextStyle(
|
title,
|
||||||
fontSize: 14.sp,
|
maxLines: 1,
|
||||||
color: const Color(0xFF7A828E),
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.sp,
|
||||||
|
color: const Color(0xFF7A828E),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
key: ValueKey('manual-process-${matchup.id}'),
|
key: ValueKey('manual-process-${matchup.id}'),
|
||||||
onPressed: onManualProcess,
|
onPressed: onManualProcess,
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import 'package:flutter_screenutil/flutter_screenutil.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.dart';
|
||||||
import 'package:recording_tool/features/competition_teams/pages/page_competition_team_detail.dart';
|
import 'package:recording_tool/features/competition_teams/pages/page_competition_team_detail.dart';
|
||||||
|
import 'package:recording_tool/features/competition_teams/server/server_competition_teams.dart';
|
||||||
import 'package:recording_tool/features/competition_teams/view_model/view_model_competition_teams.dart';
|
import 'package:recording_tool/features/competition_teams/view_model/view_model_competition_teams.dart';
|
||||||
import 'package:recording_tool/shared/widgets/app_empty_view.dart';
|
import 'package:recording_tool/shared/widgets/app_empty_view.dart';
|
||||||
import 'package:recording_tool/shared/widgets/app_error_view.dart';
|
import 'package:recording_tool/shared/widgets/app_error_view.dart';
|
||||||
import 'package:recording_tool/shared/widgets/app_loading_view.dart';
|
import 'package:recording_tool/shared/widgets/app_loading_view.dart';
|
||||||
import 'package:recording_tool/shared/widgets/app_refresh_list.dart';
|
import 'package:recording_tool/shared/widgets/app_refresh_list.dart';
|
||||||
|
import 'package:recording_tool/shared/widgets/app_toast.dart';
|
||||||
|
|
||||||
class CompetitionTeamListPage extends ConsumerStatefulWidget {
|
class CompetitionTeamListPage extends ConsumerStatefulWidget {
|
||||||
const CompetitionTeamListPage({super.key});
|
const CompetitionTeamListPage({super.key});
|
||||||
@@ -60,13 +62,23 @@ class _CompetitionTeamListPageState
|
|||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
return _CompetitionScheduleCard(
|
return _CompetitionScheduleCard(
|
||||||
item: item,
|
item: item,
|
||||||
onTap: () => AppNavigator.push(
|
onTap: () async {
|
||||||
CompetitionTeamDetailPage(
|
try {
|
||||||
itemId: item.itemId,
|
final detail = await ref
|
||||||
eventId: item.eventId,
|
.read(competitionTeamsServerProvider)
|
||||||
),
|
.fetchTeamDetail(
|
||||||
context: context,
|
itemId: item.itemId,
|
||||||
),
|
eventId: item.eventId,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
AppNavigator.push(
|
||||||
|
CompetitionTeamDetailPage(detail: detail),
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
AppToast.show('对阵详情加载失败,请重试');
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ typedef TeamQrScanner = Future<String?> Function(BuildContext context);
|
|||||||
class EventTeamMatchPage extends StatefulWidget {
|
class EventTeamMatchPage extends StatefulWidget {
|
||||||
const EventTeamMatchPage({
|
const EventTeamMatchPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
|
||||||
required this.playerId,
|
required this.playerId,
|
||||||
this.qrScanner,
|
this.qrScanner,
|
||||||
|
required this.detail,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EventRegistrationItem item;
|
final CompetitionTeamDetail detail;
|
||||||
final String playerId;
|
final String playerId;
|
||||||
final TeamQrScanner? qrScanner;
|
final TeamQrScanner? qrScanner;
|
||||||
|
|
||||||
@@ -32,55 +32,113 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
|
|||||||
final Set<String> _verifiedUserIds = <String>{};
|
final Set<String> _verifiedUserIds = <String>{};
|
||||||
String? _winnerTeamId;
|
String? _winnerTeamId;
|
||||||
|
|
||||||
CompetitionTeam get _homeTeam {
|
CompetitionTeamDetail get _detail => widget.detail;
|
||||||
final leader = widget.item.teamLeader;
|
|
||||||
final teamId = leader?.userId.isNotEmpty == true
|
/// 备注:teamA 为红队,teamB 为蓝队。
|
||||||
? leader!.userId
|
Matchup? get _firstMatchup {
|
||||||
: widget.item.userId.isNotEmpty
|
final matchups = _detail.matchups;
|
||||||
? widget.item.userId
|
if (matchups == null || matchups.isEmpty) return null;
|
||||||
: 'home-team';
|
return matchups.first;
|
||||||
final teamName = leader?.name.isNotEmpty == true
|
|
||||||
? leader!.name
|
|
||||||
: widget.item.playerName.isNotEmpty
|
|
||||||
? widget.item.playerName
|
|
||||||
: '本方队伍';
|
|
||||||
return CompetitionTeam(
|
|
||||||
id: teamId,
|
|
||||||
name: teamName,
|
|
||||||
players: widget.item.teamMembers
|
|
||||||
.map(
|
|
||||||
(member) => CompetitionPlayer(id: member.userId, name: member.name),
|
|
||||||
)
|
|
||||||
.toList(growable: false),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CompetitionTeam get _opponentTeam {
|
/// 红队(teamA)
|
||||||
final opponentId = widget.item.opponentId.isNotEmpty
|
Team? get _redRawTeam {
|
||||||
? widget.item.opponentId
|
final teams = _firstMatchup?.teamA;
|
||||||
: 'opponent-team';
|
if (teams == null || teams.isEmpty) return null;
|
||||||
final opponentName = widget.item.opponentName.isNotEmpty
|
return teams.first;
|
||||||
? widget.item.opponentName
|
|
||||||
: '对方队伍';
|
|
||||||
return CompetitionTeam(
|
|
||||||
id: opponentId,
|
|
||||||
name: opponentName,
|
|
||||||
players:
|
|
||||||
widget.item.opponentId.isEmpty && widget.item.opponentName.isEmpty
|
|
||||||
? const []
|
|
||||||
: [CompetitionPlayer(id: opponentId, name: opponentName)],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CompetitionMatchup get _matchup => CompetitionMatchup(
|
/// 蓝队(teamB)
|
||||||
id: widget.item.scheduleId.isNotEmpty
|
Team? get _blueRawTeam {
|
||||||
? widget.item.scheduleId
|
final teams = _firstMatchup?.teamB;
|
||||||
: '${widget.item.itemId}-team-match',
|
if (teams == null || teams.isEmpty) return null;
|
||||||
teamA: _homeTeam,
|
return teams.first;
|
||||||
teamB: _opponentTeam,
|
}
|
||||||
winnerTeamId: _winnerTeamId,
|
|
||||||
|
CompetitionTeam get _redTeam => _toCompetitionTeam(
|
||||||
|
_redRawTeam,
|
||||||
|
fallbackId: 'red-team',
|
||||||
|
fallbackName: '红队',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CompetitionTeam get _blueTeam => _toCompetitionTeam(
|
||||||
|
_blueRawTeam,
|
||||||
|
fallbackId: 'blue-team',
|
||||||
|
fallbackName: '蓝队',
|
||||||
|
);
|
||||||
|
|
||||||
|
List<EventTeamMember> get _redMembers =>
|
||||||
|
_toEventMembers(_redRawTeam?.players);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// H5 bridge 仍需要 EventRegistrationItem,从 detail 组装最小对象。
|
||||||
|
/// 备注:红队成员写入 teamMembers,蓝队写入 opponent。
|
||||||
|
EventRegistrationItem get _scoreBridgeItem {
|
||||||
|
final blue = _blueTeam;
|
||||||
|
return EventRegistrationItem(
|
||||||
|
eventId: _detail.eventId?.trim() ?? '',
|
||||||
|
itemId: _detail.itemId?.trim() ?? '',
|
||||||
|
scheduleId: _detail.scheduleId?.trim() ?? '',
|
||||||
|
eventName: '',
|
||||||
|
itemName: _detail.itemName?.trim() ?? '',
|
||||||
|
groupName: _detail.groupName?.trim() ?? '',
|
||||||
|
matchPlace: _detail.matchPlace?.trim() ?? '',
|
||||||
|
matchStartTime: _detail.matchStartTime?.trim() ?? '',
|
||||||
|
matchEndTime: _detail.matchEndTime?.trim() ?? '',
|
||||||
|
opponentId: blue.id == 'blue-team' ? '' : blue.id,
|
||||||
|
opponentName: blue.name == '蓝队' ? '' : blue.name,
|
||||||
|
teamMembers: _redMembers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompetitionTeam _toCompetitionTeam(
|
||||||
|
Team? team, {
|
||||||
|
required String fallbackId,
|
||||||
|
required String fallbackName,
|
||||||
|
}) {
|
||||||
|
final id = team?.id?.trim() ?? '';
|
||||||
|
final name = team?.teamName?.trim() ?? '';
|
||||||
|
final players = (team?.players ?? const <Player>[])
|
||||||
|
.map(
|
||||||
|
(player) => CompetitionPlayer(
|
||||||
|
id: player.id?.trim() ?? '',
|
||||||
|
name: player.name?.trim() ?? '',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
return CompetitionTeam(
|
||||||
|
id: id.isNotEmpty ? id : fallbackId,
|
||||||
|
name: name.isNotEmpty ? name : fallbackName,
|
||||||
|
players: players,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EventTeamMember> _toEventMembers(List<Player>? players) {
|
||||||
|
if (players == null || players.isEmpty) return const [];
|
||||||
|
return players
|
||||||
|
.map(
|
||||||
|
(player) => EventTeamMember(
|
||||||
|
userId: player.id?.trim() ?? '',
|
||||||
|
name: player.name?.trim() ?? '',
|
||||||
|
isLeader: player.isLeader ?? false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -92,18 +150,15 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
|
|||||||
|
|
||||||
bool _isKnownMember(String userId) {
|
bool _isKnownMember(String userId) {
|
||||||
if (userId.isEmpty) return false;
|
if (userId.isEmpty) return false;
|
||||||
if (userId == widget.item.opponentId) return true;
|
return _redMembers.any((member) => member.userId == userId) ||
|
||||||
return widget.item.teamMembers.any((member) => member.userId == userId);
|
_blueMembers.any((member) => member.userId == userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _memberNameOf(String userId) {
|
String _memberNameOf(String userId) {
|
||||||
if (userId == widget.item.opponentId) {
|
for (final member in [..._redMembers, ..._blueMembers]) {
|
||||||
return widget.item.opponentName.isEmpty
|
if (member.userId == userId) {
|
||||||
? '对方队长'
|
return member.name.isEmpty ? '参赛成员' : member.name;
|
||||||
: widget.item.opponentName;
|
}
|
||||||
}
|
|
||||||
for (final member in widget.item.teamMembers) {
|
|
||||||
if (member.userId == userId) return member.name;
|
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -131,23 +186,23 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
|
|||||||
);
|
);
|
||||||
if (!mounted || winnerTeamId == null) return;
|
if (!mounted || winnerTeamId == null) return;
|
||||||
setState(() => _winnerTeamId = winnerTeamId);
|
setState(() => _winnerTeamId = winnerTeamId);
|
||||||
final winnerName = winnerTeamId == _homeTeam.id
|
final winnerName = winnerTeamId == _redTeam.id
|
||||||
? _homeTeam.name
|
? _redTeam.name
|
||||||
: _opponentTeam.name;
|
: _blueTeam.name;
|
||||||
AppToast.show('已设置$winnerName直接获胜');
|
AppToast.show('已设置$winnerName直接获胜');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startDirectly() {
|
void _startDirectly() {
|
||||||
AppNavigator.push(
|
AppNavigator.push(
|
||||||
buildTeamScorePage(item: widget.item, playerId: widget.playerId),
|
buildTeamScorePage(item: _scoreBridgeItem, playerId: widget.playerId),
|
||||||
context: context,
|
context: context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final homeTeam = _homeTeam;
|
final redTeam = _redTeam;
|
||||||
final opponentTeam = _opponentTeam;
|
final blueTeam = _blueTeam;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(),
|
appBar: AppBar(),
|
||||||
@@ -161,8 +216,16 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_MatchMetadata(item: widget.item),
|
_MatchMetadata(detail: _detail),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 10.h),
|
||||||
|
Text(
|
||||||
|
'备注:上方红队(teamA),下方蓝队(teamB)',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.sp,
|
||||||
|
color: const Color(0xFF7A828E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8.h),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
@@ -179,28 +242,24 @@ class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_TeamCard(
|
_TeamCard(
|
||||||
team: homeTeam,
|
teamLabel: '红队',
|
||||||
members: widget.item.teamMembers,
|
team: redTeam,
|
||||||
|
members: _redMembers,
|
||||||
backgroundColor: const Color(0xFFFFEEF0),
|
backgroundColor: const Color(0xFFFFEEF0),
|
||||||
accentColor: const Color(0xFFE84B5B),
|
accentColor: const Color(0xFFE84B5B),
|
||||||
verifiedUserIds: _verifiedUserIds,
|
verifiedUserIds: _verifiedUserIds,
|
||||||
winner: _winnerTeamId == homeTeam.id,
|
winner: _winnerTeamId == redTeam.id,
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
_TeamCard(
|
_TeamCard(
|
||||||
team: opponentTeam,
|
teamLabel: '蓝队',
|
||||||
members: [
|
team: blueTeam,
|
||||||
EventTeamMember(
|
members: _blueMembers,
|
||||||
userId: widget.item.opponentId,
|
|
||||||
name: widget.item.opponentName,
|
|
||||||
isLeader: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
backgroundColor: const Color(0xFFEDF5FF),
|
backgroundColor: const Color(0xFFEDF5FF),
|
||||||
accentColor: const Color(0xFF287FDD),
|
accentColor: const Color(0xFF287FDD),
|
||||||
verifiedUserIds: _verifiedUserIds,
|
verifiedUserIds: _verifiedUserIds,
|
||||||
winner: _winnerTeamId == opponentTeam.id,
|
winner: _winnerTeamId == blueTeam.id,
|
||||||
emptyMessage: '暂无对方成员数据',
|
emptyMessage: '暂无蓝队成员数据',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -245,12 +304,15 @@ WebviewPage buildTeamScorePage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MatchMetadata extends StatelessWidget {
|
class _MatchMetadata extends StatelessWidget {
|
||||||
const _MatchMetadata({required this.item});
|
const _MatchMetadata({required this.detail});
|
||||||
|
|
||||||
final EventRegistrationItem item;
|
final CompetitionTeamDetail detail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final itemName = detail.itemName?.trim() ?? '';
|
||||||
|
final matchPlace = detail.matchPlace?.trim() ?? '';
|
||||||
|
final groupName = detail.groupName?.trim() ?? '';
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: EdgeInsets.all(18.r),
|
padding: EdgeInsets.all(18.r),
|
||||||
@@ -264,14 +326,14 @@ class _MatchMetadata extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _MetadataText(label: '比赛项目', value: item.itemName),
|
child: _MetadataText(label: '比赛项目', value: itemName),
|
||||||
),
|
),
|
||||||
SizedBox(width: 16.w),
|
SizedBox(width: 16.w),
|
||||||
_MetadataText(label: '场地', value: item.matchPlace),
|
_MetadataText(label: '场地', value: matchPlace),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 14.h),
|
SizedBox(height: 14.h),
|
||||||
_MetadataText(label: '组别', value: item.groupName),
|
_MetadataText(label: '组别', value: groupName),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -302,6 +364,7 @@ class _MetadataText extends StatelessWidget {
|
|||||||
|
|
||||||
class _TeamCard extends StatelessWidget {
|
class _TeamCard extends StatelessWidget {
|
||||||
const _TeamCard({
|
const _TeamCard({
|
||||||
|
required this.teamLabel,
|
||||||
required this.team,
|
required this.team,
|
||||||
required this.members,
|
required this.members,
|
||||||
required this.backgroundColor,
|
required this.backgroundColor,
|
||||||
@@ -311,6 +374,7 @@ class _TeamCard extends StatelessWidget {
|
|||||||
this.emptyMessage = '暂无成员数据',
|
this.emptyMessage = '暂无成员数据',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String teamLabel;
|
||||||
final CompetitionTeam team;
|
final CompetitionTeam team;
|
||||||
final List<EventTeamMember> members;
|
final List<EventTeamMember> members;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
@@ -343,7 +407,7 @@ class _TeamCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'队长:${team.name}',
|
'$teamLabel:${team.name}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 19.sp,
|
fontSize: 19.sp,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|||||||
@@ -28,13 +28,21 @@ class CompetitionTeamsServer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> fetchTeamDetail({
|
Future<CompetitionTeamDetail> fetchTeamDetail({
|
||||||
required String itemId,
|
required String itemId,
|
||||||
required String eventId,
|
required String eventId,
|
||||||
|
String? scheduleId,
|
||||||
|
String? opponentId,
|
||||||
}) {
|
}) {
|
||||||
return _apiClient.get(
|
return _apiClient.post(
|
||||||
AuthApi.getTeamDetail.path,
|
AuthApi.getTeamDetail.path,
|
||||||
queryParameters: {'itemId': itemId, 'eventId': eventId},
|
data: {
|
||||||
|
'itemId': itemId,
|
||||||
|
'eventId': eventId,
|
||||||
|
'scheduleId': scheduleId,
|
||||||
|
'opponentId': opponentId,
|
||||||
|
},
|
||||||
|
parser: (json) => CompetitionTeamDetail.fromResponse(json),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ 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/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/events/model/model_event_info.dart';
|
import 'package:recording_tool/features/events/model/model_event_info.dart';
|
||||||
import 'package:recording_tool/features/events/state/state_event_info.dart';
|
import 'package:recording_tool/features/events/state/state_event_info.dart';
|
||||||
import 'package:recording_tool/features/events/view_model/view_model_event_info.dart';
|
import 'package:recording_tool/features/events/view_model/view_model_event_info.dart';
|
||||||
@@ -25,13 +27,37 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onItemTap(EventRegistrationItem item) async {
|
Future<void> onItemTap(EventRegistrationItem item) async {
|
||||||
// debugPrint('item tapped: ${item.itemName}');
|
|
||||||
// await ref.read(eventInfoProvider.notifier).requestStreamKey(item);
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// final profile = ref.read(eventInfoProvider).profile;
|
CompetitionTeamDetail? detail;
|
||||||
|
if (item.opponentId.isNotEmpty && item.opponentId != '0') {
|
||||||
|
detail = await ref
|
||||||
|
.read(competitionTeamsServerProvider)
|
||||||
|
.fetchTeamDetail(
|
||||||
|
itemId: item.itemId,
|
||||||
|
eventId: item.eventId,
|
||||||
|
scheduleId: item.scheduleId,
|
||||||
|
opponentId: item.opponentId,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
AppNavigator.push(
|
||||||
|
buildEventRegistrationDestination(
|
||||||
|
item: item,
|
||||||
|
playerId: widget.playerId,
|
||||||
|
detail: detail,
|
||||||
|
),
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
AppNavigator.push(
|
AppNavigator.push(
|
||||||
buildEventRegistrationDestination(item: item, playerId: widget.playerId),
|
buildEventRegistrationDestination(
|
||||||
|
item: item,
|
||||||
|
playerId: widget.playerId,
|
||||||
|
detail: null,
|
||||||
|
),
|
||||||
context: context,
|
context: context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,9 +115,10 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
|
|||||||
Widget buildEventRegistrationDestination({
|
Widget buildEventRegistrationDestination({
|
||||||
required EventRegistrationItem item,
|
required EventRegistrationItem item,
|
||||||
required String playerId,
|
required String playerId,
|
||||||
|
required CompetitionTeamDetail? detail,
|
||||||
}) {
|
}) {
|
||||||
if (item.opponentId.isNotEmpty && item.opponentId != '0') {
|
if (detail != null) {
|
||||||
return EventTeamMatchPage(item: item, playerId: playerId);
|
return EventTeamMatchPage(playerId: playerId, detail: detail);
|
||||||
}
|
}
|
||||||
return WebviewPage(
|
return WebviewPage(
|
||||||
url: AppConfig.current.mainRefereeScoreH5Url,
|
url: AppConfig.current.mainRefereeScoreH5Url,
|
||||||
|
|||||||
Reference in New Issue
Block a user