重构参赛队伍相关模型,更新API接口以支持队伍详情获取;
调整UI逻辑以使用新的数据结构,优化比赛信息展示; 更新构建脚本以支持新功能。
This commit is contained in:
@@ -11,6 +11,9 @@ enum AuthApi {
|
||||
/// 获取参赛队伍列表
|
||||
getTeamList('/api/events/device/item/group'),
|
||||
|
||||
/// 参赛队伍详情
|
||||
getTeamDetail('/api/events/device/schedule/pending/score'),
|
||||
|
||||
/// 获取选手的赛事信息
|
||||
playerRegistrationList('/api/events/device/player/registration/list');
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_controller?.text = '555';
|
||||
// _controller?.text = '299689';
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final token = AppStorage.getString(StorageKeys.authToken);
|
||||
|
||||
@@ -42,44 +42,59 @@ class CompetitionMatchup {
|
||||
}
|
||||
}
|
||||
|
||||
/// 参赛项目/组别列表项(来自 /api/events/device/item/group)
|
||||
class CompetitionTeamListItem {
|
||||
const CompetitionTeamListItem({
|
||||
required this.id,
|
||||
required this.eventName,
|
||||
required this.itemName,
|
||||
required this.eventId,
|
||||
required this.itemId,
|
||||
required this.name,
|
||||
required this.groupName,
|
||||
required this.matchPlace,
|
||||
required this.matchStartTime,
|
||||
required this.matchEndTime,
|
||||
required this.matchups,
|
||||
this.completed = false,
|
||||
this.matchStartTime = '',
|
||||
this.matchEndTime = '',
|
||||
this.matchups = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String eventName;
|
||||
final String itemName;
|
||||
final String eventId;
|
||||
final String itemId;
|
||||
final String name;
|
||||
final String groupName;
|
||||
final String matchPlace;
|
||||
final String matchStartTime;
|
||||
final String matchEndTime;
|
||||
|
||||
/// 对阵明细暂未由列表接口返回,默认空
|
||||
final List<CompetitionMatchup> matchups;
|
||||
final bool completed;
|
||||
|
||||
String get title => groupName.isEmpty ? itemName : '$itemName ($groupName)';
|
||||
String get title => groupName.isEmpty ? name : '$name ($groupName)';
|
||||
|
||||
String get scheduleTime => '$matchStartTime-$matchEndTime';
|
||||
String get scheduleTime {
|
||||
if (matchStartTime.isEmpty && matchEndTime.isEmpty) return '';
|
||||
if (matchStartTime.isNotEmpty && matchEndTime.isNotEmpty) {
|
||||
return '$matchStartTime-$matchEndTime';
|
||||
}
|
||||
if (matchStartTime.isNotEmpty) return matchStartTime;
|
||||
return matchEndTime;
|
||||
}
|
||||
|
||||
factory CompetitionTeamListItem.fromJson(Map<String, dynamic> json) {
|
||||
return CompetitionTeamListItem(
|
||||
eventId: (json['eventId'] ?? '').toString(),
|
||||
itemId: (json['itemId'] ?? '').toString(),
|
||||
name: (json['name'] ?? '').toString(),
|
||||
groupName: (json['groupName'] ?? '').toString(),
|
||||
matchStartTime: (json['matchStartTime'] ?? '').toString(),
|
||||
matchEndTime: (json['matchEndTime'] ?? '').toString(),
|
||||
);
|
||||
}
|
||||
|
||||
CompetitionTeamListItem copyWith({List<CompetitionMatchup>? matchups}) {
|
||||
return CompetitionTeamListItem(
|
||||
id: id,
|
||||
eventName: eventName,
|
||||
itemName: itemName,
|
||||
eventId: eventId,
|
||||
itemId: itemId,
|
||||
name: name,
|
||||
groupName: groupName,
|
||||
matchPlace: matchPlace,
|
||||
matchStartTime: matchStartTime,
|
||||
matchEndTime: matchEndTime,
|
||||
matchups: matchups ?? this.matchups,
|
||||
completed: completed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -98,4 +113,59 @@ class CompetitionTeamPageResult {
|
||||
final int pageSize;
|
||||
|
||||
bool get hasMore => page * pageSize < total;
|
||||
|
||||
factory CompetitionTeamPageResult.fromJson(
|
||||
dynamic json, {
|
||||
required int page,
|
||||
required int pageSize,
|
||||
}) {
|
||||
if (json is List) {
|
||||
// 整表无分页:本页即全部,hasMore = false
|
||||
final items = json
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => CompetitionTeamListItem.fromJson(
|
||||
Map<String, dynamic>.from(item),
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
final effectivePageSize = items.isEmpty ? pageSize : items.length;
|
||||
return CompetitionTeamPageResult(
|
||||
items: items,
|
||||
total: items.length,
|
||||
page: page,
|
||||
pageSize: effectivePageSize,
|
||||
);
|
||||
}
|
||||
|
||||
if (json is Map) {
|
||||
final map = Map<String, dynamic>.from(json);
|
||||
final rawItems =
|
||||
map['items'] ?? map['rows'] ?? map['list'] ?? map['records'];
|
||||
final items = rawItems is List
|
||||
? rawItems
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => CompetitionTeamListItem.fromJson(
|
||||
Map<String, dynamic>.from(item),
|
||||
),
|
||||
)
|
||||
.toList(growable: false)
|
||||
: const <CompetitionTeamListItem>[];
|
||||
final total = (map['total'] as num?)?.toInt() ?? items.length;
|
||||
return CompetitionTeamPageResult(
|
||||
items: items,
|
||||
total: total,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
return CompetitionTeamPageResult(
|
||||
items: const [],
|
||||
total: 0,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,14 @@ import 'package:recording_tool/shared/widgets/app_empty_view.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_toast.dart';
|
||||
|
||||
class CompetitionTeamDetailPage extends ConsumerWidget {
|
||||
const CompetitionTeamDetailPage({super.key, required this.itemId});
|
||||
const CompetitionTeamDetailPage({
|
||||
super.key,
|
||||
required this.itemId,
|
||||
required this.eventId,
|
||||
});
|
||||
|
||||
final String itemId;
|
||||
final String eventId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -19,7 +24,7 @@ class CompetitionTeamDetailPage extends ConsumerWidget {
|
||||
);
|
||||
CompetitionTeamListItem? item;
|
||||
for (final candidate in items) {
|
||||
if (candidate.id == itemId) {
|
||||
if (candidate.itemId == itemId) {
|
||||
item = candidate;
|
||||
break;
|
||||
}
|
||||
@@ -30,6 +35,8 @@ class CompetitionTeamDetailPage extends ConsumerWidget {
|
||||
appBar: AppBar(title: Text(item?.title ?? '参赛队伍')),
|
||||
body: item == null
|
||||
? const AppEmptyView(message: '未找到对阵信息')
|
||||
: item.matchups.isEmpty
|
||||
? const AppEmptyView(message: '暂无对阵信息')
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 24.h, 20.w, 36.h),
|
||||
itemCount: item.matchups.length,
|
||||
@@ -42,7 +49,7 @@ class CompetitionTeamDetailPage extends ConsumerWidget {
|
||||
onManualProcess: () => _handleManualProcess(
|
||||
context,
|
||||
ref,
|
||||
itemId: item!.id,
|
||||
itemId: item!.itemId,
|
||||
matchup: matchup,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -61,7 +61,10 @@ class _CompetitionTeamListPageState
|
||||
return _CompetitionScheduleCard(
|
||||
item: item,
|
||||
onTap: () => AppNavigator.push(
|
||||
CompetitionTeamDetailPage(itemId: item.id),
|
||||
CompetitionTeamDetailPage(
|
||||
itemId: item.itemId,
|
||||
eventId: item.eventId,
|
||||
),
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
@@ -82,8 +85,10 @@ class _CompetitionScheduleCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheduleText = item.scheduleTime.isEmpty ? '时间待定' : item.scheduleTime;
|
||||
|
||||
return Material(
|
||||
key: ValueKey('competition-team-item-${item.id}'),
|
||||
key: ValueKey('competition-team-item-${item.itemId}'),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
child: InkWell(
|
||||
@@ -120,40 +125,18 @@ class _CompetitionScheduleCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18.h),
|
||||
_InfoLine(
|
||||
icon: Icons.location_on_outlined,
|
||||
text: item.matchPlace,
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
_InfoLine(
|
||||
icon: Icons.schedule_outlined,
|
||||
text: item.scheduleTime,
|
||||
text: scheduleText,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 14.w),
|
||||
Container(
|
||||
width: 72.w,
|
||||
height: 72.h,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: item.completed
|
||||
? const Color(0xFFF1F3F6)
|
||||
: const Color(0xFFEAF3FF),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
child: Text(
|
||||
item.completed ? '已完成' : item.matchPlace,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: item.completed ? 14.sp : 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: item.completed
|
||||
? const Color(0xFF69717D)
|
||||
: const Color(0xFF147FEA),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 28.r,
|
||||
color: const Color(0xFF9AA3AF),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,118 +1,40 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
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';
|
||||
|
||||
final competitionTeamsServerProvider = Provider<CompetitionTeamsServer>((ref) {
|
||||
return const CompetitionTeamsServer();
|
||||
return CompetitionTeamsServer(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
class CompetitionTeamsServer {
|
||||
const CompetitionTeamsServer();
|
||||
CompetitionTeamsServer(this._apiClient);
|
||||
|
||||
static const totalCount = 10;
|
||||
final ApiClient _apiClient;
|
||||
|
||||
Future<CompetitionTeamPageResult> fetchPage({
|
||||
required int page,
|
||||
required int pageSize,
|
||||
}) async {
|
||||
final start = (page - 1) * pageSize;
|
||||
if (start >= totalCount) {
|
||||
return CompetitionTeamPageResult(
|
||||
items: const [],
|
||||
total: totalCount,
|
||||
}) {
|
||||
return _apiClient.get(
|
||||
AuthApi.getTeamList.path,
|
||||
queryParameters: {'page': page, 'pageSize': pageSize},
|
||||
parser: (json) => CompetitionTeamPageResult.fromJson(
|
||||
json,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
final end = (start + pageSize).clamp(0, totalCount);
|
||||
final items = List.generate(
|
||||
end - start,
|
||||
(index) => _buildItem(start + index),
|
||||
);
|
||||
return CompetitionTeamPageResult(
|
||||
items: items,
|
||||
total: totalCount,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
CompetitionTeamListItem _buildItem(int index) {
|
||||
final number = index + 1;
|
||||
final group = index.isEven ? '小学组' : '初中组';
|
||||
final hour = 9 + index ~/ 2;
|
||||
return CompetitionTeamListItem(
|
||||
id: 'competition-$number',
|
||||
eventName: '全国青少年无人机大赛',
|
||||
itemName: index % 3 == 0 ? '空中足球赛' : '空中格斗赛',
|
||||
groupName: group,
|
||||
matchPlace: '场地 ${index % 4 + 1}',
|
||||
matchStartTime: '${hour.toString().padLeft(2, '0')}:00',
|
||||
matchEndTime: '${hour.toString().padLeft(2, '0')}:30',
|
||||
completed: index == totalCount - 1,
|
||||
matchups: [
|
||||
_buildMatchup(itemNumber: number, matchNumber: 1),
|
||||
_buildMatchup(itemNumber: number, matchNumber: 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
CompetitionMatchup _buildMatchup({
|
||||
required int itemNumber,
|
||||
required int matchNumber,
|
||||
}) {
|
||||
final matchupId = 'match-$itemNumber-$matchNumber';
|
||||
final teamAIndex = (itemNumber + matchNumber - 2) % _teamNames.length;
|
||||
final teamBIndex = (teamAIndex + 1) % _teamNames.length;
|
||||
return CompetitionMatchup(
|
||||
id: matchupId,
|
||||
teamA: _buildTeam(
|
||||
id: '$matchupId-team-a',
|
||||
name: _teamNames[teamAIndex],
|
||||
playerOffset: teamAIndex * 3,
|
||||
),
|
||||
teamB: _buildTeam(
|
||||
id: '$matchupId-team-b',
|
||||
name: _teamNames[teamBIndex],
|
||||
playerOffset: teamBIndex * 3,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
CompetitionTeam _buildTeam({
|
||||
required String id,
|
||||
required String name,
|
||||
required int playerOffset,
|
||||
Future<dynamic> fetchTeamDetail({
|
||||
required String itemId,
|
||||
required String eventId,
|
||||
}) {
|
||||
return CompetitionTeam(
|
||||
id: id,
|
||||
name: name,
|
||||
players: List.generate(3, (index) {
|
||||
final playerIndex = (playerOffset + index) % _playerNames.length;
|
||||
return CompetitionPlayer(
|
||||
id: '$id-player-$index',
|
||||
name: _playerNames[playerIndex],
|
||||
);
|
||||
}),
|
||||
return _apiClient.get(
|
||||
AuthApi.getTeamDetail.path,
|
||||
queryParameters: {'itemId': itemId, 'eventId': eventId},
|
||||
);
|
||||
}
|
||||
|
||||
static const _teamNames = ['王多鱼队', '钱多多队', '逐风少年队', '蓝翼飞行队', '星火队'];
|
||||
static const _playerNames = [
|
||||
'王伟',
|
||||
'李庆超',
|
||||
'王大亮',
|
||||
'韩宁政',
|
||||
'阮晴桦',
|
||||
'刘美玲',
|
||||
'陈子航',
|
||||
'周白芷',
|
||||
'林培伦',
|
||||
'蔡依婷',
|
||||
'夏志豪',
|
||||
'赵云飞',
|
||||
'孙雨泽',
|
||||
'吴佳琪',
|
||||
'郑凯文',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class CompetitionTeamsViewModel extends StateNotifier<CompetitionTeamsState> {
|
||||
}) {
|
||||
final updatedItems = state.items
|
||||
.map((item) {
|
||||
if (item.id != itemId) return item;
|
||||
if (item.itemId != itemId) return item;
|
||||
final updatedMatchups = item.matchups
|
||||
.map((matchup) {
|
||||
if (matchup.id != matchupId) return matchup;
|
||||
@@ -67,7 +67,7 @@ class CompetitionTeamsViewModel extends StateNotifier<CompetitionTeamsState> {
|
||||
|
||||
CompetitionTeamListItem? findItem(String itemId) {
|
||||
for (final item in state.items) {
|
||||
if (item.id == itemId) return item;
|
||||
if (item.itemId == itemId) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user