重构参赛队伍相关模型,更新API接口以支持队伍详情获取;
调整UI逻辑以使用新的数据结构,优化比赛信息展示; 更新构建脚本以支持新功能。
This commit is contained in:
+4
-2
@@ -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 "新增可视化请求插件"
|
||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user