Compare commits
2
Commits
a4333d3bd1
...
e5b6246458
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5b6246458 | ||
|
|
4e0a675198 |
@@ -8,11 +8,13 @@ class EnvironmentValues {
|
||||
required this.environment,
|
||||
required this.baseUrl,
|
||||
required this.enableNetworkLog,
|
||||
this.mainRefereeScoreH5Url = '',
|
||||
});
|
||||
|
||||
final AppEnvironment environment;
|
||||
final String baseUrl;
|
||||
final bool enableNetworkLog;
|
||||
final String mainRefereeScoreH5Url;
|
||||
}
|
||||
|
||||
class AppConfig {
|
||||
@@ -24,6 +26,8 @@ class AppConfig {
|
||||
static const appName = '飞行极控录像工作台';
|
||||
static const designSize = Size(375, 812);
|
||||
|
||||
/// 主裁判计分 H5 链接
|
||||
|
||||
static void configure({
|
||||
required AppEnvironment environment,
|
||||
AppPackageInfo? packageInfo,
|
||||
@@ -34,16 +38,22 @@ class AppConfig {
|
||||
environment: AppEnvironment.dev,
|
||||
// baseUrl: 'http://192.168.1.104:8000',
|
||||
baseUrl: 'https://apitest.dronex.cc',
|
||||
mainRefereeScoreH5Url:
|
||||
'https://drone.apptest.sportsx.cc/#/pages/h5/score-entry',
|
||||
enableNetworkLog: true,
|
||||
),
|
||||
AppEnvironment.staging => const EnvironmentValues(
|
||||
environment: AppEnvironment.staging,
|
||||
baseUrl: 'https://apitest.dronex.cc',
|
||||
mainRefereeScoreH5Url:
|
||||
'https://drone.apptest.sportsx.cc/#/pages/h5/score-entry',
|
||||
enableNetworkLog: true,
|
||||
),
|
||||
AppEnvironment.prod => const EnvironmentValues(
|
||||
environment: AppEnvironment.prod,
|
||||
baseUrl: 'https://api.dronex.cc',
|
||||
mainRefereeScoreH5Url:
|
||||
'https://drone.sportsx.cc/#/pages/h5/score-entry',
|
||||
enableNetworkLog: false,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
class CompetitionPlayer {
|
||||
const CompetitionPlayer({required this.id, required this.name});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
}
|
||||
|
||||
class CompetitionTeam {
|
||||
const CompetitionTeam({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.players,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final List<CompetitionPlayer> players;
|
||||
|
||||
String get playerNames => players.map((player) => player.name).join('、');
|
||||
}
|
||||
|
||||
class CompetitionMatchup {
|
||||
const CompetitionMatchup({
|
||||
required this.id,
|
||||
required this.teamA,
|
||||
required this.teamB,
|
||||
this.winnerTeamId,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final CompetitionTeam teamA;
|
||||
final CompetitionTeam teamB;
|
||||
final String? winnerTeamId;
|
||||
|
||||
CompetitionMatchup copyWith({String? winnerTeamId}) {
|
||||
return CompetitionMatchup(
|
||||
id: id,
|
||||
teamA: teamA,
|
||||
teamB: teamB,
|
||||
winnerTeamId: winnerTeamId ?? this.winnerTeamId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CompetitionTeamListItem {
|
||||
const CompetitionTeamListItem({
|
||||
required this.id,
|
||||
required this.eventName,
|
||||
required this.itemName,
|
||||
required this.groupName,
|
||||
required this.matchPlace,
|
||||
required this.matchStartTime,
|
||||
required this.matchEndTime,
|
||||
required this.matchups,
|
||||
this.completed = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String eventName;
|
||||
final String itemName;
|
||||
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 scheduleTime => '$matchStartTime-$matchEndTime';
|
||||
|
||||
CompetitionTeamListItem copyWith({List<CompetitionMatchup>? matchups}) {
|
||||
return CompetitionTeamListItem(
|
||||
id: id,
|
||||
eventName: eventName,
|
||||
itemName: itemName,
|
||||
groupName: groupName,
|
||||
matchPlace: matchPlace,
|
||||
matchStartTime: matchStartTime,
|
||||
matchEndTime: matchEndTime,
|
||||
matchups: matchups ?? this.matchups,
|
||||
completed: completed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CompetitionTeamPageResult {
|
||||
const CompetitionTeamPageResult({
|
||||
required this.items,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
});
|
||||
|
||||
final List<CompetitionTeamListItem> items;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
|
||||
bool get hasMore => page * pageSize < total;
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/view_model/view_model_competition_teams.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_toast.dart';
|
||||
|
||||
class CompetitionTeamDetailPage extends ConsumerWidget {
|
||||
const CompetitionTeamDetailPage({super.key, required this.itemId});
|
||||
|
||||
final String itemId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final items = ref.watch(
|
||||
competitionTeamsProvider.select((state) => state.items),
|
||||
);
|
||||
CompetitionTeamListItem? item;
|
||||
for (final candidate in items) {
|
||||
if (candidate.id == itemId) {
|
||||
item = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(title: Text(item?.title ?? '参赛队伍')),
|
||||
body: item == null
|
||||
? const AppEmptyView(message: '未找到对阵信息')
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 24.h, 20.w, 36.h),
|
||||
itemCount: item.matchups.length,
|
||||
separatorBuilder: (_, _) => SizedBox(height: 22.h),
|
||||
itemBuilder: (context, index) {
|
||||
final matchup = item!.matchups[index];
|
||||
return _MatchupCard(
|
||||
matchup: matchup,
|
||||
index: index,
|
||||
onManualProcess: () => _handleManualProcess(
|
||||
context,
|
||||
ref,
|
||||
itemId: item!.id,
|
||||
matchup: matchup,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleManualProcess(
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required String itemId,
|
||||
required CompetitionMatchup matchup,
|
||||
}) async {
|
||||
final winnerTeamId = await ManualWinnerDialog.show(
|
||||
context,
|
||||
matchup: matchup,
|
||||
);
|
||||
if (winnerTeamId == null || !context.mounted) return;
|
||||
|
||||
ref
|
||||
.read(competitionTeamsProvider.notifier)
|
||||
.selectWinner(
|
||||
itemId: itemId,
|
||||
matchupId: matchup.id,
|
||||
winnerTeamId: winnerTeamId,
|
||||
);
|
||||
final winnerName = winnerTeamId == matchup.teamA.id
|
||||
? matchup.teamA.name
|
||||
: matchup.teamB.name;
|
||||
AppToast.show('已设置$winnerName直接获胜');
|
||||
}
|
||||
}
|
||||
|
||||
class _MatchupCard extends StatelessWidget {
|
||||
const _MatchupCard({
|
||||
required this.matchup,
|
||||
required this.index,
|
||||
required this.onManualProcess,
|
||||
});
|
||||
|
||||
final CompetitionMatchup matchup;
|
||||
final int index;
|
||||
final VoidCallback onManualProcess;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
key: ValueKey('competition-matchup-${matchup.id}'),
|
||||
padding: EdgeInsets.fromLTRB(16.w, 10.h, 16.w, 18.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: const Color(0xFFC7CCD4)),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'第 ${index + 1} 场',
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: const Color(0xFF7A828E),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
key: ValueKey('manual-process-${matchup.id}'),
|
||||
onPressed: onManualProcess,
|
||||
child: Text(
|
||||
'人工处理',
|
||||
style: TextStyle(
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF078AF2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _TeamPanel(
|
||||
team: matchup.teamA,
|
||||
alignment: CrossAxisAlignment.start,
|
||||
winner: matchup.winnerTeamId == matchup.teamA.id,
|
||||
accentColor: const Color(0xFFFF6B75),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: Text(
|
||||
'VS',
|
||||
style: TextStyle(
|
||||
fontSize: 21.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF303640),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _TeamPanel(
|
||||
team: matchup.teamB,
|
||||
alignment: CrossAxisAlignment.end,
|
||||
textAlign: TextAlign.end,
|
||||
winner: matchup.winnerTeamId == matchup.teamB.id,
|
||||
accentColor: const Color(0xFF20BFA9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TeamPanel extends StatelessWidget {
|
||||
const _TeamPanel({
|
||||
required this.team,
|
||||
required this.alignment,
|
||||
required this.winner,
|
||||
required this.accentColor,
|
||||
this.textAlign = TextAlign.start,
|
||||
});
|
||||
|
||||
final CompetitionTeam team;
|
||||
final CrossAxisAlignment alignment;
|
||||
final TextAlign textAlign;
|
||||
final bool winner;
|
||||
final Color accentColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: alignment,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
|
||||
decoration: BoxDecoration(
|
||||
color: winner
|
||||
? accentColor.withValues(alpha: 0.14)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
team.name,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF282E37),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (winner) ...[
|
||||
SizedBox(width: 5.w),
|
||||
Icon(Icons.emoji_events, size: 17.r, color: accentColor),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
team.playerNames,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
height: 1.45,
|
||||
color: const Color(0xFF4E5662),
|
||||
),
|
||||
),
|
||||
if (winner) ...[
|
||||
SizedBox(height: 8.h),
|
||||
Container(
|
||||
key: ValueKey('winner-${team.id}'),
|
||||
padding: EdgeInsets.symmetric(horizontal: 9.w, vertical: 3.h),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Text(
|
||||
'胜方',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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_competition_team_detail.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_error_view.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_loading_view.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_refresh_list.dart';
|
||||
|
||||
class CompetitionTeamListPage extends ConsumerStatefulWidget {
|
||||
const CompetitionTeamListPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CompetitionTeamListPage> createState() =>
|
||||
_CompetitionTeamListPageState();
|
||||
}
|
||||
|
||||
class _CompetitionTeamListPageState
|
||||
extends ConsumerState<CompetitionTeamListPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(competitionTeamsProvider.notifier).loadInitial();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(competitionTeamsProvider);
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(title: const Text('参赛队伍')),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (state.isInitialLoading && state.items.isEmpty) {
|
||||
return const AppLoadingView(message: '正在加载参赛队伍...');
|
||||
}
|
||||
if (state.errorMessage != null && state.items.isEmpty) {
|
||||
return AppErrorView(
|
||||
message: state.errorMessage!,
|
||||
onRetry: () =>
|
||||
ref.read(competitionTeamsProvider.notifier).loadInitial(),
|
||||
);
|
||||
}
|
||||
|
||||
return AppRefreshList<CompetitionTeamListItem>(
|
||||
items: state.items,
|
||||
onRefresh: ref.read(competitionTeamsProvider.notifier).refresh,
|
||||
onLoadMore: ref.read(competitionTeamsProvider.notifier).loadMore,
|
||||
enablePullUp: state.hasMore,
|
||||
padding: EdgeInsets.fromLTRB(20.w, 18.h, 20.w, 28.h),
|
||||
separator: SizedBox(height: 14.h),
|
||||
empty: const AppEmptyView(message: '暂无参赛队伍'),
|
||||
itemBuilder: (context, item, index) {
|
||||
return _CompetitionScheduleCard(
|
||||
item: item,
|
||||
onTap: () => AppNavigator.push(
|
||||
CompetitionTeamDetailPage(itemId: item.id),
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CompetitionScheduleCard extends StatelessWidget {
|
||||
const _CompetitionScheduleCard({required this.item, required this.onTap});
|
||||
|
||||
final CompetitionTeamListItem item;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
key: ValueKey('competition-team-item-${item.id}'),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: 142.h),
|
||||
padding: EdgeInsets.all(16.r),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
border: Border.all(color: const Color(0xFFD7DBE2)),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0F1A2230),
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF20242B),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoLine extends StatelessWidget {
|
||||
const _InfoLine({required this.icon, required this.text});
|
||||
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 18.r, color: const Color(0xFF7B8491)),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 15.sp, color: const Color(0xFF525B68)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:recording_tool/app/config/app_config.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/widgets/widget_manual_winner_dialog.dart';
|
||||
import 'package:recording_tool/features/events/model/model_event_info.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_toast.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_webview.dart';
|
||||
|
||||
typedef TeamQrScanner = Future<String?> Function(BuildContext context);
|
||||
|
||||
class EventTeamMatchPage extends StatefulWidget {
|
||||
const EventTeamMatchPage({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.playerId,
|
||||
this.qrScanner,
|
||||
});
|
||||
|
||||
final EventRegistrationItem item;
|
||||
final String playerId;
|
||||
final TeamQrScanner? qrScanner;
|
||||
|
||||
@override
|
||||
State<EventTeamMatchPage> createState() => _EventTeamMatchPageState();
|
||||
}
|
||||
|
||||
class _EventTeamMatchPageState extends State<EventTeamMatchPage> {
|
||||
final Set<String> _verifiedUserIds = <String>{};
|
||||
String? _winnerTeamId;
|
||||
|
||||
CompetitionTeam get _homeTeam {
|
||||
final leader = widget.item.teamLeader;
|
||||
final teamId = leader?.userId.isNotEmpty == true
|
||||
? leader!.userId
|
||||
: widget.item.userId.isNotEmpty
|
||||
? widget.item.userId
|
||||
: 'home-team';
|
||||
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 {
|
||||
final opponentId = widget.item.opponentId.isNotEmpty
|
||||
? widget.item.opponentId
|
||||
: 'opponent-team';
|
||||
final opponentName = widget.item.opponentName.isNotEmpty
|
||||
? 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(
|
||||
id: widget.item.scheduleId.isNotEmpty
|
||||
? widget.item.scheduleId
|
||||
: '${widget.item.itemId}-team-match',
|
||||
teamA: _homeTeam,
|
||||
teamB: _opponentTeam,
|
||||
winnerTeamId: _winnerTeamId,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialPlayerId = widget.playerId.trim();
|
||||
if (_isKnownMember(initialPlayerId)) {
|
||||
_verifiedUserIds.add(initialPlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isKnownMember(String userId) {
|
||||
if (userId.isEmpty) return false;
|
||||
if (userId == widget.item.opponentId) return true;
|
||||
return widget.item.teamMembers.any((member) => member.userId == userId);
|
||||
}
|
||||
|
||||
String _memberNameOf(String userId) {
|
||||
if (userId == widget.item.opponentId) {
|
||||
return widget.item.opponentName.isEmpty
|
||||
? '对方队长'
|
||||
: widget.item.opponentName;
|
||||
}
|
||||
for (final member in widget.item.teamMembers) {
|
||||
if (member.userId == userId) return member.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> _continueScan() async {
|
||||
final scan = widget.qrScanner ?? AppQrScannerDialog.show;
|
||||
final result = (await scan(context))?.trim();
|
||||
if (!mounted || result == null || result.isEmpty) return;
|
||||
if (!_isKnownMember(result)) {
|
||||
AppToast.show('未找到对应参赛成员');
|
||||
return;
|
||||
}
|
||||
if (_verifiedUserIds.contains(result)) {
|
||||
AppToast.show('${_memberNameOf(result)}已完成核验');
|
||||
return;
|
||||
}
|
||||
setState(() => _verifiedUserIds.add(result));
|
||||
AppToast.show('${_memberNameOf(result)}核验成功');
|
||||
}
|
||||
|
||||
Future<void> _manualProcess() async {
|
||||
final winnerTeamId = await ManualWinnerDialog.show(
|
||||
context,
|
||||
matchup: _matchup,
|
||||
);
|
||||
if (!mounted || winnerTeamId == null) return;
|
||||
setState(() => _winnerTeamId = winnerTeamId);
|
||||
final winnerName = winnerTeamId == _homeTeam.id
|
||||
? _homeTeam.name
|
||||
: _opponentTeam.name;
|
||||
AppToast.show('已设置$winnerName直接获胜');
|
||||
}
|
||||
|
||||
void _startDirectly() {
|
||||
AppNavigator.push(
|
||||
buildTeamScorePage(item: widget.item, playerId: widget.playerId),
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final homeTeam = _homeTeam;
|
||||
final opponentTeam = _opponentTeam;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 20.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_MatchMetadata(item: widget.item),
|
||||
SizedBox(height: 16.h),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
key: const ValueKey('event-team-manual-process'),
|
||||
onPressed: _manualProcess,
|
||||
child: Text(
|
||||
'人工处理',
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF078AF2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_TeamCard(
|
||||
team: homeTeam,
|
||||
members: widget.item.teamMembers,
|
||||
backgroundColor: const Color(0xFFFFEEF0),
|
||||
accentColor: const Color(0xFFE84B5B),
|
||||
verifiedUserIds: _verifiedUserIds,
|
||||
winner: _winnerTeamId == homeTeam.id,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
_TeamCard(
|
||||
team: opponentTeam,
|
||||
members: [
|
||||
EventTeamMember(
|
||||
userId: widget.item.opponentId,
|
||||
name: widget.item.opponentName,
|
||||
isLeader: true,
|
||||
),
|
||||
],
|
||||
backgroundColor: const Color(0xFFEDF5FF),
|
||||
accentColor: const Color(0xFF287FDD),
|
||||
verifiedUserIds: _verifiedUserIds,
|
||||
winner: _winnerTeamId == opponentTeam.id,
|
||||
emptyMessage: '暂无对方成员数据',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 20.h),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: AppButton(
|
||||
label: '继续扫码',
|
||||
variant: AppButtonVariant.outline,
|
||||
onPressed: _continueScan,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: AppButton(label: '直接开赛', onPressed: _startDirectly),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WebviewPage buildTeamScorePage({
|
||||
required EventRegistrationItem item,
|
||||
required String playerId,
|
||||
}) {
|
||||
return WebviewPage(
|
||||
url: AppConfig.current.mainRefereeScoreH5Url,
|
||||
eventRegistrationItem: item,
|
||||
playerId: playerId,
|
||||
);
|
||||
}
|
||||
|
||||
class _MatchMetadata extends StatelessWidget {
|
||||
const _MatchMetadata({required this.item});
|
||||
|
||||
final EventRegistrationItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(18.r),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF7F9FC),
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _MetadataText(label: '比赛项目', value: item.itemName),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
_MetadataText(label: '场地', value: item.matchPlace),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
_MetadataText(label: '组别', value: item.groupName),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MetadataText extends StatelessWidget {
|
||||
const _MetadataText({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
'$label:${value.isEmpty ? '暂无' : value}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 17.sp,
|
||||
height: 1.35,
|
||||
color: const Color(0xFF303640),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TeamCard extends StatelessWidget {
|
||||
const _TeamCard({
|
||||
required this.team,
|
||||
required this.members,
|
||||
required this.backgroundColor,
|
||||
required this.accentColor,
|
||||
required this.verifiedUserIds,
|
||||
required this.winner,
|
||||
this.emptyMessage = '暂无成员数据',
|
||||
});
|
||||
|
||||
final CompetitionTeam team;
|
||||
final List<EventTeamMember> members;
|
||||
final Color backgroundColor;
|
||||
final Color accentColor;
|
||||
final Set<String> verifiedUserIds;
|
||||
final bool winner;
|
||||
final String emptyMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final visibleMembers = members
|
||||
.where((member) => member.userId.isNotEmpty || member.name.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(18.r),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
border: Border.all(
|
||||
color: winner ? accentColor : accentColor.withValues(alpha: 0.24),
|
||||
width: winner ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'队长:${team.name}',
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF252B34),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (winner)
|
||||
Container(
|
||||
key: ValueKey('event-team-winner-${team.id}'),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10.w,
|
||||
vertical: 4.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Text(
|
||||
'胜方',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
if (visibleMembers.isEmpty)
|
||||
Text(
|
||||
emptyMessage,
|
||||
style: TextStyle(fontSize: 15.sp, color: const Color(0xFF747D89)),
|
||||
)
|
||||
else
|
||||
...visibleMembers.map(
|
||||
(member) => _MemberRow(
|
||||
member: member,
|
||||
verified: verifiedUserIds.contains(member.userId),
|
||||
accentColor: accentColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MemberRow extends StatelessWidget {
|
||||
const _MemberRow({
|
||||
required this.member,
|
||||
required this.verified,
|
||||
required this.accentColor,
|
||||
});
|
||||
|
||||
final EventTeamMember member;
|
||||
final bool verified;
|
||||
final Color accentColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 5.h),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6.r,
|
||||
height: 6.r,
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${member.isLeader ? '队长' : '选手'}:${member.name.isEmpty ? '暂无' : member.name}',
|
||||
style: TextStyle(fontSize: 16.sp, color: const Color(0xFF303640)),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child: verified
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
key: ValueKey('verified-member-${member.userId}'),
|
||||
color: const Color(0xFF18A957),
|
||||
size: 24.r,
|
||||
)
|
||||
: SizedBox(width: 24.r, height: 24.r),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||
|
||||
final competitionTeamsServerProvider = Provider<CompetitionTeamsServer>((ref) {
|
||||
return const CompetitionTeamsServer();
|
||||
});
|
||||
|
||||
class CompetitionTeamsServer {
|
||||
const CompetitionTeamsServer();
|
||||
|
||||
static const totalCount = 10;
|
||||
|
||||
Future<CompetitionTeamPageResult> fetchPage({
|
||||
required int page,
|
||||
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';
|
||||
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,
|
||||
}) {
|
||||
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],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static const _teamNames = ['王多鱼队', '钱多多队', '逐风少年队', '蓝翼飞行队', '星火队'];
|
||||
static const _playerNames = [
|
||||
'王伟',
|
||||
'李庆超',
|
||||
'王大亮',
|
||||
'韩宁政',
|
||||
'阮晴桦',
|
||||
'刘美玲',
|
||||
'陈子航',
|
||||
'周白芷',
|
||||
'林培伦',
|
||||
'蔡依婷',
|
||||
'夏志豪',
|
||||
'赵云飞',
|
||||
'孙雨泽',
|
||||
'吴佳琪',
|
||||
'郑凯文',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||
|
||||
class CompetitionTeamsState {
|
||||
const CompetitionTeamsState({
|
||||
this.items = const [],
|
||||
this.page = 0,
|
||||
this.hasMore = true,
|
||||
this.isInitialLoading = false,
|
||||
this.isRefreshing = false,
|
||||
this.isLoadingMore = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final List<CompetitionTeamListItem> items;
|
||||
final int page;
|
||||
final bool hasMore;
|
||||
final bool isInitialLoading;
|
||||
final bool isRefreshing;
|
||||
final bool isLoadingMore;
|
||||
final String? errorMessage;
|
||||
|
||||
CompetitionTeamsState copyWith({
|
||||
List<CompetitionTeamListItem>? items,
|
||||
int? page,
|
||||
bool? hasMore,
|
||||
bool? isInitialLoading,
|
||||
bool? isRefreshing,
|
||||
bool? isLoadingMore,
|
||||
String? errorMessage,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return CompetitionTeamsState(
|
||||
items: items ?? this.items,
|
||||
page: page ?? this.page,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
isInitialLoading: isInitialLoading ?? this.isInitialLoading,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
errorMessage: clearError ? null : errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/legacy.dart';
|
||||
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||
import 'package:recording_tool/features/competition_teams/server/server_competition_teams.dart';
|
||||
import 'package:recording_tool/features/competition_teams/state/state_competition_teams.dart';
|
||||
|
||||
final competitionTeamsProvider =
|
||||
StateNotifierProvider<CompetitionTeamsViewModel, CompetitionTeamsState>((
|
||||
ref,
|
||||
) {
|
||||
return CompetitionTeamsViewModel(ref);
|
||||
});
|
||||
|
||||
class CompetitionTeamsViewModel extends StateNotifier<CompetitionTeamsState> {
|
||||
CompetitionTeamsViewModel(this._ref) : super(const CompetitionTeamsState());
|
||||
|
||||
static const pageSize = 5;
|
||||
|
||||
final Ref _ref;
|
||||
|
||||
Future<void> loadInitial() async {
|
||||
state = state.copyWith(
|
||||
isInitialLoading: true,
|
||||
isLoadingMore: false,
|
||||
isRefreshing: false,
|
||||
clearError: true,
|
||||
);
|
||||
await _loadPage(page: 1, replace: true);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (state.isRefreshing) return;
|
||||
state = state.copyWith(isRefreshing: true, clearError: true);
|
||||
await _loadPage(page: 1, replace: true);
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (!state.hasMore || state.isLoadingMore || state.isInitialLoading) return;
|
||||
state = state.copyWith(isLoadingMore: true, clearError: true);
|
||||
await _loadPage(page: state.page + 1, replace: false);
|
||||
}
|
||||
|
||||
void selectWinner({
|
||||
required String itemId,
|
||||
required String matchupId,
|
||||
required String winnerTeamId,
|
||||
}) {
|
||||
final updatedItems = state.items
|
||||
.map((item) {
|
||||
if (item.id != itemId) return item;
|
||||
final updatedMatchups = item.matchups
|
||||
.map((matchup) {
|
||||
if (matchup.id != matchupId) return matchup;
|
||||
final validWinner =
|
||||
winnerTeamId == matchup.teamA.id ||
|
||||
winnerTeamId == matchup.teamB.id;
|
||||
return validWinner
|
||||
? matchup.copyWith(winnerTeamId: winnerTeamId)
|
||||
: matchup;
|
||||
})
|
||||
.toList(growable: false);
|
||||
return item.copyWith(matchups: updatedMatchups);
|
||||
})
|
||||
.toList(growable: false);
|
||||
state = state.copyWith(items: updatedItems);
|
||||
}
|
||||
|
||||
CompetitionTeamListItem? findItem(String itemId) {
|
||||
for (final item in state.items) {
|
||||
if (item.id == itemId) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _loadPage({required int page, required bool replace}) async {
|
||||
try {
|
||||
final result = await _ref
|
||||
.read(competitionTeamsServerProvider)
|
||||
.fetchPage(page: page, pageSize: pageSize);
|
||||
state = state.copyWith(
|
||||
items: replace ? result.items : [...state.items, ...result.items],
|
||||
page: result.page,
|
||||
hasMore: result.hasMore,
|
||||
isInitialLoading: false,
|
||||
isRefreshing: false,
|
||||
isLoadingMore: false,
|
||||
clearError: true,
|
||||
);
|
||||
} catch (error) {
|
||||
state = state.copyWith(
|
||||
isInitialLoading: false,
|
||||
isRefreshing: false,
|
||||
isLoadingMore: false,
|
||||
errorMessage: '参赛队伍加载失败,请重试',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:recording_tool/features/competition_teams/model/model_competition_team.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_button.dart';
|
||||
|
||||
class ManualWinnerDialog extends StatefulWidget {
|
||||
const ManualWinnerDialog({super.key, required this.matchup});
|
||||
|
||||
final CompetitionMatchup matchup;
|
||||
|
||||
static Future<String?> show(
|
||||
BuildContext context, {
|
||||
required CompetitionMatchup matchup,
|
||||
}) {
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => ManualWinnerDialog(matchup: matchup),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<ManualWinnerDialog> createState() => _ManualWinnerDialogState();
|
||||
}
|
||||
|
||||
class _ManualWinnerDialogState extends State<ManualWinnerDialog> {
|
||||
String? _selectedTeamId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedTeamId = widget.matchup.winnerTeamId;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
insetPadding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.r)),
|
||||
content: SizedBox(
|
||||
width: 320.w,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/image_dialog_bg.png',
|
||||
width: 320.w,
|
||||
height: 112.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 20.h),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'请在比赛结束前处理',
|
||||
key: const ValueKey('manual-winner-dialog-title'),
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF20242B),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
_TeamChoice(
|
||||
team: widget.matchup.teamA,
|
||||
color: const Color(0xFFFF6B75),
|
||||
selected: _selectedTeamId == widget.matchup.teamA.id,
|
||||
onTap: () => setState(
|
||||
() => _selectedTeamId = widget.matchup.teamA.id,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
_TeamChoice(
|
||||
team: widget.matchup.teamB,
|
||||
color: const Color(0xFF20BFA9),
|
||||
selected: _selectedTeamId == widget.matchup.teamB.id,
|
||||
onTap: () => setState(
|
||||
() => _selectedTeamId = widget.matchup.teamB.id,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
label: '取消',
|
||||
variant: AppButtonVariant.secondary,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 14.w),
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
label: '确定',
|
||||
onPressed: _selectedTeamId == null
|
||||
? null
|
||||
: () => Navigator.of(
|
||||
context,
|
||||
).pop(_selectedTeamId),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TeamChoice extends StatelessWidget {
|
||||
const _TeamChoice({
|
||||
required this.team,
|
||||
required this.color,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CompetitionTeam team;
|
||||
final Color color;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
selected: selected,
|
||||
button: true,
|
||||
label: '选择${team.name}直接获胜',
|
||||
child: Material(
|
||||
color: color.withValues(alpha: selected ? 0.16 : 0.08),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: InkWell(
|
||||
key: ValueKey('winner-choice-${team.id}'),
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(
|
||||
color: selected ? color : color.withValues(alpha: 0.35),
|
||||
width: selected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: 22.r,
|
||||
height: 22.r,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: selected ? color : Colors.grey),
|
||||
color: selected ? color : Colors.transparent,
|
||||
),
|
||||
child: selected
|
||||
? Icon(Icons.check, size: 15.r, color: Colors.white)
|
||||
: null,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
team.name,
|
||||
style: TextStyle(
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF20242B),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
team.playerNames,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: const Color(0xFF606874),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
'直接获胜',
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,27 @@ class EventProfile {
|
||||
final String avatarLabel;
|
||||
}
|
||||
|
||||
class EventTeamMember {
|
||||
const EventTeamMember({
|
||||
required this.userId,
|
||||
required this.name,
|
||||
required this.isLeader,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final String name;
|
||||
final bool isLeader;
|
||||
|
||||
factory EventTeamMember.fromJson(Map<dynamic, dynamic> json) {
|
||||
final map = Map<String, dynamic>.from(json);
|
||||
return EventTeamMember(
|
||||
userId: _readString(map, const ['userId']),
|
||||
name: _readString(map, const ['name']),
|
||||
isLeader: _readBool(map, const ['isLeader']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventRegistrationList {
|
||||
const EventRegistrationList({
|
||||
required this.userId,
|
||||
@@ -111,7 +132,7 @@ class EventRegistrationItem {
|
||||
this.completed = false,
|
||||
this.opponentId = '',
|
||||
this.opponentName = '',
|
||||
this.teamMembers,
|
||||
this.teamMembers = const [],
|
||||
this.userId = '',
|
||||
this.playerName = '',
|
||||
this.playerPhone = '',
|
||||
@@ -129,18 +150,24 @@ class EventRegistrationItem {
|
||||
final bool completed;
|
||||
final String opponentId;
|
||||
final String opponentName;
|
||||
final List<dynamic>? teamMembers;
|
||||
final List<EventTeamMember> teamMembers;
|
||||
|
||||
/// 来自报名列表父级,不在 item JSON 内
|
||||
final String userId;
|
||||
final String playerName;
|
||||
final String playerPhone;
|
||||
|
||||
String get scheduleTime =>
|
||||
_formatScheduleTime(matchStartTime, matchEndTime);
|
||||
String get scheduleTime => _formatScheduleTime(matchStartTime, matchEndTime);
|
||||
|
||||
String? get statusLabel => completed ? '已完成' : null;
|
||||
|
||||
EventTeamMember? get teamLeader {
|
||||
for (final member in teamMembers) {
|
||||
if (member.isLeader) return member;
|
||||
}
|
||||
return teamMembers.isEmpty ? null : teamMembers.first;
|
||||
}
|
||||
|
||||
factory EventRegistrationItem.fromJson(
|
||||
Map<dynamic, dynamic> json, {
|
||||
String userId = '',
|
||||
@@ -164,8 +191,11 @@ class EventRegistrationItem {
|
||||
opponentId: _readString(map, const ['opponentId']),
|
||||
opponentName: _readString(map, const ['opponentName']),
|
||||
teamMembers: teamMembersRaw is List
|
||||
? List<dynamic>.from(teamMembersRaw)
|
||||
: null,
|
||||
? teamMembersRaw
|
||||
.whereType<Map>()
|
||||
.map(EventTeamMember.fromJson)
|
||||
.toList(growable: false)
|
||||
: const [],
|
||||
userId: userId,
|
||||
playerName: playerName,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:recording_tool/app/config/app_config.dart';
|
||||
import 'package:recording_tool/app/router/app_navigator.dart';
|
||||
import 'package:recording_tool/features/competition_teams/pages/page_event_team_match.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/view_model/view_model_event_info.dart';
|
||||
@@ -28,13 +30,9 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
|
||||
if (!mounted) return;
|
||||
// final profile = ref.read(eventInfoProvider).profile;
|
||||
|
||||
/// 个人赛
|
||||
AppNavigator.push(
|
||||
WebviewPage(
|
||||
url: 'https://drone.apptest.sportsx.cc/#/pages/h5/score-entry',
|
||||
eventRegistrationItem: item,
|
||||
playerId: widget.playerId,
|
||||
),
|
||||
buildEventRegistrationDestination(item: item, playerId: widget.playerId),
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,6 +86,20 @@ class _EventInfoPageState extends ConsumerState<EventInfoPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildEventRegistrationDestination({
|
||||
required EventRegistrationItem item,
|
||||
required String playerId,
|
||||
}) {
|
||||
if (item.opponentId.isNotEmpty) {
|
||||
return EventTeamMatchPage(item: item, playerId: playerId);
|
||||
}
|
||||
return WebviewPage(
|
||||
url: AppConfig.current.mainRefereeScoreH5Url,
|
||||
eventRegistrationItem: item,
|
||||
playerId: playerId,
|
||||
);
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header({required this.onBack});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:recording_tool/core/cache/app_storage.dart';
|
||||
import 'package:recording_tool/core/cache/storage_keys.dart';
|
||||
import 'package:recording_tool/features/auth/pages/page_auth.dart';
|
||||
import 'package:recording_tool/features/auth/view_model_auth/view_model_auth.dart';
|
||||
import 'package:recording_tool/features/competition_teams/pages/page_competition_team_list.dart';
|
||||
import 'package:recording_tool/features/events/model/model_event_info.dart';
|
||||
import 'package:recording_tool/features/events/pages/page_event_info.dart';
|
||||
import 'package:recording_tool/features/events/view_model/view_model_event_info.dart';
|
||||
@@ -112,7 +113,12 @@ class _AuthPageWidgetState extends ConsumerState<ScanQrCodePage> {
|
||||
height: 80.h,
|
||||
child: AppButton(
|
||||
label: '参赛队伍',
|
||||
onPressed: () async {},
|
||||
onPressed: () async {
|
||||
await AppNavigator.push(
|
||||
const CompetitionTeamListPage(),
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
variant: AppButtonVariant.secondary,
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user