重构参赛队伍相关模型,更新API接口以支持队伍详情获取;

调整UI逻辑以使用新的数据结构,优化比赛信息展示;
更新构建脚本以支持新功能。
This commit is contained in:
2026-07-22 13:35:53 +08:00
parent d6e80df5bf
commit 1264ebdd7c
8 changed files with 140 additions and 153 deletions
+4 -2
View File
@@ -1,6 +1,8 @@
#!/bin/sh #!/bin/sh
set -e set -e
flutter build apk --release --split-per-abi # flutter build apk --release --split-per-abi
echo "构建完成时间: $(date '+%Y-%m-%d %H:%M:%S')" # echo "构建完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
pgyer upload build/app/outputs/flutter-apk/app-arm64-v8a-release.apk --build-update-description "新增可视化请求插件"
+3
View File
@@ -11,6 +11,9 @@ enum AuthApi {
/// 获取参赛队伍列表 /// 获取参赛队伍列表
getTeamList('/api/events/device/item/group'), getTeamList('/api/events/device/item/group'),
/// 参赛队伍详情
getTeamDetail('/api/events/device/schedule/pending/score'),
/// 获取选手的赛事信息 /// 获取选手的赛事信息
playerRegistrationList('/api/events/device/player/registration/list'); playerRegistrationList('/api/events/device/player/registration/list');
+1 -1
View File
@@ -23,7 +23,7 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
void initState() { void initState() {
super.initState(); super.initState();
_controller = TextEditingController(); _controller = TextEditingController();
_controller?.text = '555'; // _controller?.text = '299689';
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final token = AppStorage.getString(StorageKeys.authToken); final token = AppStorage.getString(StorageKeys.authToken);
@@ -42,44 +42,59 @@ class CompetitionMatchup {
} }
} }
/// 参赛项目/组别列表项(来自 /api/events/device/item/group
class CompetitionTeamListItem { class CompetitionTeamListItem {
const CompetitionTeamListItem({ const CompetitionTeamListItem({
required this.id, required this.eventId,
required this.eventName, required this.itemId,
required this.itemName, required this.name,
required this.groupName, required this.groupName,
required this.matchPlace, this.matchStartTime = '',
required this.matchStartTime, this.matchEndTime = '',
required this.matchEndTime, this.matchups = const [],
required this.matchups,
this.completed = false,
}); });
final String id; final String eventId;
final String eventName; final String itemId;
final String itemName; final String name;
final String groupName; final String groupName;
final String matchPlace;
final String matchStartTime; final String matchStartTime;
final String matchEndTime; final String matchEndTime;
/// 对阵明细暂未由列表接口返回,默认空
final List<CompetitionMatchup> matchups; 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}) { CompetitionTeamListItem copyWith({List<CompetitionMatchup>? matchups}) {
return CompetitionTeamListItem( return CompetitionTeamListItem(
id: id, eventId: eventId,
eventName: eventName, itemId: itemId,
itemName: itemName, name: name,
groupName: groupName, groupName: groupName,
matchPlace: matchPlace,
matchStartTime: matchStartTime, matchStartTime: matchStartTime,
matchEndTime: matchEndTime, matchEndTime: matchEndTime,
matchups: matchups ?? this.matchups, matchups: matchups ?? this.matchups,
completed: completed,
); );
} }
} }
@@ -98,4 +113,59 @@ class CompetitionTeamPageResult {
final int pageSize; final int pageSize;
bool get hasMore => page * pageSize < total; 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'; import 'package:recording_tool/shared/widgets/app_toast.dart';
class CompetitionTeamDetailPage extends ConsumerWidget { 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 itemId;
final String eventId;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -19,7 +24,7 @@ class CompetitionTeamDetailPage extends ConsumerWidget {
); );
CompetitionTeamListItem? item; CompetitionTeamListItem? item;
for (final candidate in items) { for (final candidate in items) {
if (candidate.id == itemId) { if (candidate.itemId == itemId) {
item = candidate; item = candidate;
break; break;
} }
@@ -30,6 +35,8 @@ class CompetitionTeamDetailPage extends ConsumerWidget {
appBar: AppBar(title: Text(item?.title ?? '参赛队伍')), appBar: AppBar(title: Text(item?.title ?? '参赛队伍')),
body: item == null body: item == null
? const AppEmptyView(message: '未找到对阵信息') ? const AppEmptyView(message: '未找到对阵信息')
: item.matchups.isEmpty
? 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: item.matchups.length,
@@ -42,7 +49,7 @@ class CompetitionTeamDetailPage extends ConsumerWidget {
onManualProcess: () => _handleManualProcess( onManualProcess: () => _handleManualProcess(
context, context,
ref, ref,
itemId: item!.id, itemId: item!.itemId,
matchup: matchup, matchup: matchup,
), ),
); );
@@ -61,7 +61,10 @@ class _CompetitionTeamListPageState
return _CompetitionScheduleCard( return _CompetitionScheduleCard(
item: item, item: item,
onTap: () => AppNavigator.push( onTap: () => AppNavigator.push(
CompetitionTeamDetailPage(itemId: item.id), CompetitionTeamDetailPage(
itemId: item.itemId,
eventId: item.eventId,
),
context: context, context: context,
), ),
); );
@@ -82,8 +85,10 @@ class _CompetitionScheduleCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheduleText = item.scheduleTime.isEmpty ? '时间待定' : item.scheduleTime;
return Material( return Material(
key: ValueKey('competition-team-item-${item.id}'), key: ValueKey('competition-team-item-${item.itemId}'),
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14.r), borderRadius: BorderRadius.circular(14.r),
child: InkWell( child: InkWell(
@@ -120,40 +125,18 @@ class _CompetitionScheduleCard extends StatelessWidget {
), ),
), ),
SizedBox(height: 18.h), SizedBox(height: 18.h),
_InfoLine(
icon: Icons.location_on_outlined,
text: item.matchPlace,
),
SizedBox(height: 12.h),
_InfoLine( _InfoLine(
icon: Icons.schedule_outlined, icon: Icons.schedule_outlined,
text: item.scheduleTime, text: scheduleText,
), ),
], ],
), ),
), ),
SizedBox(width: 14.w), SizedBox(width: 14.w),
Container( Icon(
width: 72.w, Icons.chevron_right,
height: 72.h, size: 28.r,
alignment: Alignment.center, color: const Color(0xFF9AA3AF),
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),
),
),
), ),
], ],
), ),
@@ -1,118 +1,40 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; 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'; import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
final competitionTeamsServerProvider = Provider<CompetitionTeamsServer>((ref) { final competitionTeamsServerProvider = Provider<CompetitionTeamsServer>((ref) {
return const CompetitionTeamsServer(); return CompetitionTeamsServer(ref.watch(apiClientProvider));
}); });
class CompetitionTeamsServer { class CompetitionTeamsServer {
const CompetitionTeamsServer(); CompetitionTeamsServer(this._apiClient);
static const totalCount = 10; final ApiClient _apiClient;
Future<CompetitionTeamPageResult> fetchPage({ Future<CompetitionTeamPageResult> fetchPage({
required int page, required int page,
required int pageSize, required int pageSize,
}) async {
final start = (page - 1) * pageSize;
if (start >= totalCount) {
return CompetitionTeamPageResult(
items: const [],
total: totalCount,
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'; return _apiClient.get(
final teamAIndex = (itemNumber + matchNumber - 2) % _teamNames.length; AuthApi.getTeamList.path,
final teamBIndex = (teamAIndex + 1) % _teamNames.length; queryParameters: {'page': page, 'pageSize': pageSize},
return CompetitionMatchup( parser: (json) => CompetitionTeamPageResult.fromJson(
id: matchupId, json,
teamA: _buildTeam( page: page,
id: '$matchupId-team-a', pageSize: pageSize,
name: _teamNames[teamAIndex],
playerOffset: teamAIndex * 3,
),
teamB: _buildTeam(
id: '$matchupId-team-b',
name: _teamNames[teamBIndex],
playerOffset: teamBIndex * 3,
), ),
); );
} }
CompetitionTeam _buildTeam({ Future<dynamic> fetchTeamDetail({
required String id, required String itemId,
required String name, required String eventId,
required int playerOffset,
}) { }) {
return CompetitionTeam( return _apiClient.get(
id: id, AuthApi.getTeamDetail.path,
name: name, queryParameters: {'itemId': itemId, 'eventId': eventId},
players: List.generate(3, (index) {
final playerIndex = (playerOffset + index) % _playerNames.length;
return CompetitionPlayer(
id: '$id-player-$index',
name: _playerNames[playerIndex],
);
}),
); );
} }
static const _teamNames = ['王多鱼队', '钱多多队', '逐风少年队', '蓝翼飞行队', '星火队'];
static const _playerNames = [
'王伟',
'李庆超',
'王大亮',
'韩宁政',
'阮晴桦',
'刘美玲',
'陈子航',
'周白芷',
'林培伦',
'蔡依婷',
'夏志豪',
'赵云飞',
'孙雨泽',
'吴佳琪',
'郑凯文',
];
} }
@@ -47,7 +47,7 @@ class CompetitionTeamsViewModel extends StateNotifier<CompetitionTeamsState> {
}) { }) {
final updatedItems = state.items final updatedItems = state.items
.map((item) { .map((item) {
if (item.id != itemId) return item; if (item.itemId != itemId) return item;
final updatedMatchups = item.matchups final updatedMatchups = item.matchups
.map((matchup) { .map((matchup) {
if (matchup.id != matchupId) return matchup; if (matchup.id != matchupId) return matchup;
@@ -67,7 +67,7 @@ class CompetitionTeamsViewModel extends StateNotifier<CompetitionTeamsState> {
CompetitionTeamListItem? findItem(String itemId) { CompetitionTeamListItem? findItem(String itemId) {
for (final item in state.items) { for (final item in state.items) {
if (item.id == itemId) return item; if (item.itemId == itemId) return item;
} }
return null; return null;
} }