更新应用配置以添加主裁判计分 H5 链接;
重构事件注册模型以支持团队成员信息,并简化事件信息页面的导航逻辑。
This commit is contained in:
@@ -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,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user