From 1c909277ff52570a493cc9b36c4ef27995c0140d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E9=94=8B?= <2535831261@qq.com> Date: Thu, 9 Jul 2026 17:38:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20devtools=5Foptions.yaml=20?= =?UTF-8?q?=E4=BB=A5=E5=90=AF=E7=94=A8=20riverpod=20=E5=92=8C=20shared=5Fp?= =?UTF-8?q?references=20=E6=89=A9=E5=B1=95=EF=BC=9B=E6=9B=B4=E6=96=B0=20Au?= =?UTF-8?q?thApi=20=E6=9E=9A=E4=B8=BE=E4=BB=A5=E6=B7=BB=E5=8A=A0=20playerR?= =?UTF-8?q?egistrationList=20API=EF=BC=9B=E9=87=8D=E6=9E=84=20Auth=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=BB=A5=E5=9C=A8=E7=94=A8=E6=88=B7=E5=B7=B2?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=97=B6=E7=9B=B4=E6=8E=A5=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E5=88=B0=E6=89=AB=E7=A0=81=E9=A1=B5=E9=9D=A2=EF=BC=9B=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20EventInfoPage=20=E4=BB=A5=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E8=B5=9B=E4=BA=8B=E6=8A=A5=E5=90=8D=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=EF=BC=9B=E6=9B=B4=E6=96=B0=E6=89=AB=E7=A0=81=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BB=A5=E5=AF=BC=E8=88=AA=E5=88=B0=20EventInfoPage?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devtools_options.yaml | 2 + lib/app/config/api_common.dart | 5 +- lib/app/router/app_navigator.dart | 5 +- lib/features/auth/pages/page_auth.dart | 13 +- .../events/model/model_event_info.dart | 356 ++++++++++++++++++ .../events/pages/page_event_info.dart | 205 +++++----- lib/features/events/server/server_events.dart | 37 ++ .../events/state/state_event_info.dart | 44 +++ .../view_model/view_model_event_info.dart | 85 +++++ .../scan_qrcode/pages/page_scan_qrcode.dart | 28 +- lib/shared/widgets/app_qr_scanner_dialog.dart | 3 + 11 files changed, 646 insertions(+), 137 deletions(-) create mode 100644 lib/features/events/model/model_event_info.dart create mode 100644 lib/features/events/server/server_events.dart create mode 100644 lib/features/events/state/state_event_info.dart create mode 100644 lib/features/events/view_model/view_model_event_info.dart diff --git a/devtools_options.yaml b/devtools_options.yaml index fa0b357..17fa745 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -1,3 +1,5 @@ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: + - riverpod: true + - shared_preferences: true \ No newline at end of file diff --git a/lib/app/config/api_common.dart b/lib/app/config/api_common.dart index 99505ed..a5cc3f6 100644 --- a/lib/app/config/api_common.dart +++ b/lib/app/config/api_common.dart @@ -3,7 +3,10 @@ enum AuthApi { getToken('/api/events/device/token'), /// 获取推流地址 - getSteamKey('api/events/device/stream/key'); + getStreamKey('/api/events/device/stream/key'), + + /// 获取选手的赛事信息 + playerRegistrationList('/api/events/device/player/registration/list'); final String path; const AuthApi(this.path); diff --git a/lib/app/router/app_navigator.dart b/lib/app/router/app_navigator.dart index 63d5e98..a517ab4 100644 --- a/lib/app/router/app_navigator.dart +++ b/lib/app/router/app_navigator.dart @@ -103,10 +103,7 @@ class AppNavigator { } static void pop({BuildContext? context, T? result}) { - Navigator.of( - context ?? AppNavigator.context!, - rootNavigator: true, - ).pop(result); + Navigator.maybePop(context ?? AppNavigator.context!); } static void popTimes({BuildContext? context, int count = 1}) { diff --git a/lib/features/auth/pages/page_auth.dart b/lib/features/auth/pages/page_auth.dart index 2b9774f..9e3d184 100644 --- a/lib/features/auth/pages/page_auth.dart +++ b/lib/features/auth/pages/page_auth.dart @@ -2,8 +2,10 @@ 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/core/cache/app_storage.dart'; +import 'package:recording_tool/core/cache/storage_keys.dart'; import 'package:recording_tool/features/auth/view_model_auth/view_model_auth.dart'; -import 'package:recording_tool/features/events/pages/page_event_info.dart'; +import 'package:recording_tool/features/scan_qrcode/pages/page_scan_qrcode.dart'; import 'package:recording_tool/shared/widgets/widgets.dart'; class AuthPageWidget extends ConsumerStatefulWidget { @@ -21,6 +23,13 @@ class _AuthPageWidgetState extends ConsumerState { super.initState(); _controller = TextEditingController(); _controller?.text = '555'; + + WidgetsBinding.instance.addPostFrameCallback((_) { + final token = AppStorage.getString(StorageKeys.authToken); + if (token?.isNotEmpty ?? false) { + AppNavigator.pushReplacement(const ScanQrCodePage()); + } + }); } @override @@ -62,7 +71,7 @@ class _AuthPageWidgetState extends ConsumerState { .auth(code ?? ''); if (!mounted) return; if (success) { - AppNavigator.push(const EventInfoPage()); + AppNavigator.push(const ScanQrCodePage()); return; } final message = ref.read(authProvider).errorMessage; diff --git a/lib/features/events/model/model_event_info.dart b/lib/features/events/model/model_event_info.dart new file mode 100644 index 0000000..d725139 --- /dev/null +++ b/lib/features/events/model/model_event_info.dart @@ -0,0 +1,356 @@ +import 'package:recording_tool/features/recording/model/model_recording_context.dart'; + +class PlayerRegistrationListReq { + const PlayerRegistrationListReq({required this.userId, this.status = ''}); + + final String userId; + final String status; + + Map toFormDataMap() { + return {'userId': userId, 'status': status}; + } +} + +class StreamKeyReq { + const StreamKeyReq({ + required this.eventId, + required this.itemId, + required this.userId, + }); + + final String eventId; + final String itemId; + final String userId; + + Map toFormDataMap() { + return {'eventId': eventId, 'itemId': itemId, 'userId': userId}; + } +} + +class EventProfile { + const EventProfile({ + required this.name, + required this.phone, + required this.eventTitle, + this.avatarUrl = '', + this.avatarLabel = '头像', + }); + + final String name; + final String phone; + final String eventTitle; + final String avatarUrl; + final String avatarLabel; +} + +class EventRegistrationList { + const EventRegistrationList({ + required this.userId, + required this.name, + required this.avatar, + required this.total, + required this.items, + }); + + final String userId; + final String name; + final String avatar; + final int total; + final List items; + + factory EventRegistrationList.fromJson(dynamic json) { + final map = _extractObject(json); + final rawItems = _extractList(json); + final userId = _readString(map, const ['userId']); + final name = _readString(map, const [ + 'name', + 'playerName', + 'userName', + 'realName', + ]); + final avatar = _readString(map, const ['avatar', 'avatarUrl']); + return EventRegistrationList( + userId: userId, + name: name, + avatar: avatar, + total: _readInt(map, const ['total'], fallback: rawItems.length), + items: rawItems + .whereType() + .map( + (item) => EventRegistrationItem.fromJson( + item, + userId: userId, + playerName: name, + ), + ) + .toList(), + ); + } + + EventProfile toProfile() { + return EventProfile( + name: name, + phone: '', + eventTitle: items.isEmpty ? '赛事信息' : items.first.eventTitle, + avatarUrl: avatar, + ); + } +} + +class EventRegistrationItem { + const EventRegistrationItem({ + required this.eventId, + required this.itemId, + required this.userId, + required this.eventTitle, + required this.name, + required this.group, + required this.venue, + required this.time, + required this.playerName, + required this.playerPhone, + this.avatarLabel = '头像', + this.matchStartTime = '', + this.matchEndTime = '', + this.completed = false, + this.laneNo, + this.status, + this.rawData = const {}, + }); + + final String eventId; + final String itemId; + final String userId; + final String eventTitle; + final String name; + final String group; + final String venue; + final String time; + final String playerName; + final String playerPhone; + final String avatarLabel; + final String matchStartTime; + final String matchEndTime; + final bool completed; + final String? laneNo; + final String? status; + final Map rawData; + + factory EventRegistrationItem.fromJson( + Map json, { + String userId = '', + String playerName = '', + }) { + final map = Map.from(json); + final startTime = _readString(map, const ['matchStartTime', 'startTime']); + final endTime = _readString(map, const ['matchEndTime', 'endTime']); + final completed = _readBool(map, const ['completed']); + return EventRegistrationItem( + eventId: _readString(map, const ['eventId', 'eventsId', 'eventID']), + itemId: _readString(map, const ['itemId', 'eventItemId', 'itemID']), + userId: _readString(map, const [ + 'userId', + 'playerId', + 'registrationId', + ], fallback: userId), + eventTitle: _readString(map, const [ + 'eventTitle', + 'eventName', + 'competitionName', + 'matchTitle', + ], fallback: '赛事信息'), + name: _readString(map, const [ + 'name', + 'itemName', + 'eventItemName', + 'matchName', + 'projectName', + ], fallback: '未命名项目'), + group: _readString(map, const [ + 'group', + 'groupName', + 'categoryName', + 'levelName', + ]), + venue: _readString(map, const [ + 'matchPlace', + 'venue', + 'venueName', + 'siteName', + 'fieldName', + 'placeName', + ]), + time: _readScheduleTime(map, startTime, endTime), + playerName: _readString(map, const [ + 'playerName', + 'userName', + 'nameCn', + 'realName', + 'athleteName', + ], fallback: playerName), + playerPhone: _readString(map, const [ + 'playerPhone', + 'phone', + 'mobile', + 'telephone', + 'userPhone', + ]), + laneNo: _readNullableString(map, const [ + 'laneNo', + 'laneNumber', + 'trackNo', + 'number', + 'serialNo', + ]), + matchStartTime: startTime, + matchEndTime: endTime, + completed: completed, + status: completed ? '已完成' : null, + rawData: map, + ); + } + + EventProfile toProfile() { + return EventProfile( + avatarLabel: avatarLabel, + name: playerName, + phone: playerPhone, + eventTitle: eventTitle, + ); + } + + RecordingContext toRecordingContext({EventProfile? profile}) { + return RecordingContext( + eventTitle: eventTitle, + matchName: name, + group: group, + venue: venue, + time: time, + playerName: profile?.name ?? playerName, + playerPhone: profile?.phone ?? playerPhone, + laneNo: laneNo, + status: status, + ); + } +} + +class StreamKeyResponse { + const StreamKeyResponse({required this.rawData}); + + final Map rawData; + + factory StreamKeyResponse.fromJson(dynamic json) { + if (json is Map) { + return StreamKeyResponse(rawData: Map.from(json)); + } + return StreamKeyResponse(rawData: {'value': json}); + } + + @override + String toString() => rawData.toString(); +} + +List _extractList(dynamic json) { + if (json is List) return json; + if (json is Map) { + for (final key in const ['records', 'items', 'rows', 'list', 'data']) { + final value = json[key]; + if (value is List) return value; + if (value is Map) { + final nested = _extractList(value); + if (nested.isNotEmpty) return nested; + } + } + } + return const []; +} + +Map _extractObject(dynamic json) { + if (json is Map) { + final map = Map.from(json); + final data = map['data']; + if (data is Map) return Map.from(data); + return map; + } + return const {}; +} + +String _readString( + Map map, + List keys, { + String fallback = '', +}) { + return _readNullableString(map, keys) ?? fallback; +} + +String _readScheduleTime( + Map map, + String startTime, + String endTime, +) { + if (startTime.isNotEmpty && endTime.isNotEmpty) { + return '$startTime-$endTime'; + } + if (startTime.isNotEmpty) return startTime; + if (endTime.isNotEmpty) return endTime; + return _readString(map, const [ + 'time', + 'competitionTime', + 'matchTime', + 'scheduleTime', + ]); +} + +int _readInt(Map map, List keys, {int fallback = 0}) { + for (final key in keys) { + final value = _findValue(map, key); + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) { + final parsed = int.tryParse(value.trim()); + if (parsed != null) return parsed; + } + } + return fallback; +} + +bool _readBool(Map map, List keys) { + for (final key in keys) { + final value = _findValue(map, key); + if (value is bool) return value; + if (value is num) return value != 0; + if (value is String) { + final text = value.trim().toLowerCase(); + if (text == 'true' || text == '1') return true; + if (text == 'false' || text == '0') return false; + } + } + return false; +} + +String? _readNullableString(Map map, List keys) { + for (final key in keys) { + final value = _findValue(map, key); + if (value == null) continue; + final text = value.toString().trim(); + if (text.isNotEmpty) return text; + } + return null; +} + +dynamic _findValue(dynamic value, String key) { + if (value is Map) { + if (value.containsKey(key)) return value[key]; + for (final child in value.values) { + final found = _findValue(child, key); + if (found != null) return found; + } + } + if (value is List) { + for (final child in value) { + final found = _findValue(child, key); + if (found != null) return found; + } + } + return null; +} diff --git a/lib/features/events/pages/page_event_info.dart b/lib/features/events/pages/page_event_info.dart index f51fde9..4d7220f 100644 --- a/lib/features/events/pages/page_event_info.dart +++ b/lib/features/events/pages/page_event_info.dart @@ -1,64 +1,51 @@ 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/recording/model/model_recording_context.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'; import 'package:recording_tool/features/recording/pages/page_record.dart'; +import 'package:recording_tool/shared/widgets/widgets.dart'; -class EventInfoPage extends StatelessWidget { - const EventInfoPage({super.key}); - +class EventInfoPage extends ConsumerStatefulWidget { + const EventInfoPage({super.key, required this.playerId}); + final String playerId; static const mockRtmpUrl = 'rtmp://192.168.1.245:19090/蔡依婷vs夏志豪_空中格斗赛_高中组/蔡依婷vs夏志豪_空中格斗赛_高中组'; - static const _profile = EventProfile( - avatarLabel: '头像', - name: '王东方', - phone: '199966666527', - eventTitle: '全国青少年无人机大赛', - ); + @override + ConsumerState createState() => _EventInfoPageState(); +} - static const _items = [ - EventScheduleItem( - name: '个人飞行赛', - group: '小学组', - venue: '场地 1', - time: '7月1日 12:00-15:00', - laneNo: '10号', - ), - EventScheduleItem( - name: '空中足球赛', - group: '小学组', - venue: '场地 1', - time: '7月2日 12:00-15:00', - status: '已完成', - ), - EventScheduleItem( - name: '穿越竞速赛', - group: '小学组', - venue: '场地 2', - time: '7月3日 09:00-11:30', - laneNo: '06号', - ), - EventScheduleItem( - name: '编队飞行赛', - group: '小学组', - venue: '场地 3', - time: '7月3日 14:00-16:00', - status: '待开始', - ), - ]; - void onItemTap(EventScheduleItem item) { +class _EventInfoPageState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + ref.read(eventInfoProvider.notifier).loadRegistrationList(); + }); + } + + Future onItemTap(EventRegistrationItem item) async { debugPrint('item tapped: ${item.name}'); + await ref.read(eventInfoProvider.notifier).requestStreamKey(item); + if (!mounted) return; + final profile = ref.read(eventInfoProvider).profile; AppNavigator.push( RecordingPage( - recordingContext: item.toRecordingContext(profile: _profile), - streamUrl: mockRtmpUrl, + recordingContext: item.toRecordingContext(profile: profile), + streamUrl: EventInfoPage.mockRtmpUrl, ), ); } @override Widget build(BuildContext context) { + final state = ref.watch(eventInfoProvider); + final profile = state.profile; + return Scaffold( backgroundColor: Colors.white, body: SafeArea( @@ -71,10 +58,10 @@ class EventInfoPage extends StatelessWidget { padding: EdgeInsets.fromLTRB(62.w, 8.h, 62.w, 40.h), child: Column( children: [ - const _ProfileSection(profile: _profile), - SizedBox(height: 82.h), + _ProfileSection(profile: profile), + SizedBox(height: 20.h), Text( - _profile.eventTitle, + state.eventTitle, style: TextStyle( fontSize: 20.sp, height: 1.2, @@ -85,12 +72,12 @@ class EventInfoPage extends StatelessWidget { SizedBox(height: 24.h), SizedBox( height: 400.h, - child: ListView.builder( - itemCount: _items.length, - itemBuilder: (context, index) => _ScheduleCard( - item: _items[index], - onTap: () => onItemTap(_items[index]), - ), + child: _ScheduleList( + state: state, + onRetry: () => ref + .read(eventInfoProvider.notifier) + .loadRegistrationList(), + onItemTap: onItemTap, ), ), ], @@ -104,52 +91,6 @@ class EventInfoPage extends StatelessWidget { } } -class EventProfile { - const EventProfile({ - required this.avatarLabel, - required this.name, - required this.phone, - required this.eventTitle, - }); - - final String avatarLabel; - final String name; - final String phone; - final String eventTitle; -} - -class EventScheduleItem { - const EventScheduleItem({ - required this.name, - required this.group, - required this.venue, - required this.time, - this.laneNo, - this.status, - }); - - final String name; - final String group; - final String venue; - final String time; - final String? laneNo; - final String? status; - - RecordingContext toRecordingContext({required EventProfile profile}) { - return RecordingContext( - eventTitle: profile.eventTitle, - matchName: name, - group: group, - venue: venue, - time: time, - playerName: profile.name, - playerPhone: profile.phone, - laneNo: laneNo, - status: status, - ); - } -} - class _Header extends StatelessWidget { const _Header({required this.onBack}); @@ -184,18 +125,10 @@ class _ProfileSection extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - // width: 225.w, - // height: 184.h, - alignment: Alignment.center, - decoration: BoxDecoration( - border: Border.all(color: const Color(0xFF7A7A7A)), - ), - child: Text( - profile.avatarLabel, - style: TextStyle(fontSize: 20.sp, color: const Color(0xFF2F2F2F)), - ), - ), + profile.avatarUrl.isEmpty + ? AppAvatar(size: 50.r) + : AppAvatar(size: 50.r, imageUrl: profile.avatarUrl), + SizedBox(width: 28.w), Expanded( child: Padding( @@ -233,10 +166,54 @@ class _ProfileSection extends StatelessWidget { } } +class _ScheduleList extends StatelessWidget { + const _ScheduleList({ + required this.state, + required this.onRetry, + required this.onItemTap, + }); + + final EventInfoState state; + final VoidCallback onRetry; + final ValueChanged onItemTap; + + @override + Widget build(BuildContext context) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.items.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + state.errorMessage ?? '暂无赛事报名信息', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18.sp, color: const Color(0xFF2F2F2F)), + ), + SizedBox(height: 18.h), + TextButton(onPressed: onRetry, child: const Text('重新加载')), + ], + ), + ); + } + + return ListView.builder( + itemCount: state.items.length, + itemBuilder: (context, index) { + final item = state.items[index]; + return _ScheduleCard(item: item, onTap: () => onItemTap(item)); + }, + ); + } +} + class _ScheduleCard extends StatelessWidget { const _ScheduleCard({required this.item, required this.onTap}); - final EventScheduleItem item; + final EventRegistrationItem item; final VoidCallback onTap; @override @@ -259,7 +236,9 @@ class _ScheduleCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${item.name} (${item.group})', + item.group.isEmpty + ? item.name + : '${item.name} (${item.group})', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -314,7 +293,7 @@ class _ScheduleCard extends StatelessWidget { class _ScheduleBadge extends StatelessWidget { const _ScheduleBadge({required this.item}); - final EventScheduleItem item; + final EventRegistrationItem item; @override Widget build(BuildContext context) { diff --git a/lib/features/events/server/server_events.dart b/lib/features/events/server/server_events.dart new file mode 100644 index 0000000..f7772b1 --- /dev/null +++ b/lib/features/events/server/server_events.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.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/http_method.dart'; +import 'package:recording_tool/core/network/providers/dio_providers.dart'; +import 'package:recording_tool/features/events/model/model_event_info.dart'; + +final eventsServerProvider = Provider((ref) { + return EventsServer(ref.watch(apiClientProvider)); +}); + +class EventsServer { + const EventsServer(this._apiClient); + + final ApiClient _apiClient; + + Future fetchPlayerRegistrationList( + PlayerRegistrationListReq req, + ) { + return _apiClient.request( + AuthApi.playerRegistrationList.path, + method: HttpMethod.get, + data: FormData.fromMap(req.toFormDataMap()), + parser: EventRegistrationList.fromJson, + ); + } + + Future fetchStreamKey(StreamKeyReq req) { + return _apiClient.request( + AuthApi.getStreamKey.path, + method: HttpMethod.get, + data: FormData.fromMap(req.toFormDataMap()), + parser: StreamKeyResponse.fromJson, + ); + } +} diff --git a/lib/features/events/state/state_event_info.dart b/lib/features/events/state/state_event_info.dart new file mode 100644 index 0000000..ac5c827 --- /dev/null +++ b/lib/features/events/state/state_event_info.dart @@ -0,0 +1,44 @@ +import 'package:recording_tool/features/events/model/model_event_info.dart'; + +class EventInfoState { + const EventInfoState({ + this.isLoading = false, + this.isRequestingStreamKey = false, + this.errorMessage, + this.profile = const EventProfile(name: '', phone: '', eventTitle: '赛事信息'), + this.total = 0, + this.items = const [], + }); + + final bool isLoading; + final bool isRequestingStreamKey; + final String? errorMessage; + final EventProfile profile; + final int total; + final List items; + + String get eventTitle => + items.isEmpty ? profile.eventTitle : items.first.eventTitle; + + EventInfoState copyWith({ + bool? isLoading, + bool? isRequestingStreamKey, + String? errorMessage, + bool clearErrorMessage = false, + EventProfile? profile, + int? total, + List? items, + }) { + return EventInfoState( + isLoading: isLoading ?? this.isLoading, + isRequestingStreamKey: + isRequestingStreamKey ?? this.isRequestingStreamKey, + errorMessage: clearErrorMessage + ? null + : (errorMessage ?? this.errorMessage), + profile: profile ?? this.profile, + total: total ?? this.total, + items: items ?? this.items, + ); + } +} diff --git a/lib/features/events/view_model/view_model_event_info.dart b/lib/features/events/view_model/view_model_event_info.dart new file mode 100644 index 0000000..d8e6051 --- /dev/null +++ b/lib/features/events/view_model/view_model_event_info.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:recording_tool/core/network/api_exception.dart'; +import 'package:recording_tool/features/events/model/model_event_info.dart'; +import 'package:recording_tool/features/events/server/server_events.dart'; +import 'package:recording_tool/features/events/state/state_event_info.dart'; + +final eventInfoProvider = + StateNotifierProvider((ref) { + return EventInfoViewModel(ref); + }); + +class EventInfoViewModel extends StateNotifier { + EventInfoViewModel(this._ref) : super(const EventInfoState()); + + static const defaultRegistrationRequest = PlayerRegistrationListReq( + userId: '74300708945530955', + status: '', + ); + + static const _fallbackEventId = '10224'; + static const _fallbackItemId = '2318'; + static const _fallbackStreamUserId = '74300708949725207'; + + final Ref _ref; + + Future loadRegistrationList({ + PlayerRegistrationListReq request = defaultRegistrationRequest, + }) async { + state = state.copyWith(isLoading: true, clearErrorMessage: true); + try { + final result = await _ref + .read(eventsServerProvider) + .fetchPlayerRegistrationList(request); + state = state.copyWith( + isLoading: false, + profile: result.toProfile(), + total: result.total, + items: result.items, + clearErrorMessage: true, + ); + } on ApiException catch (error) { + state = state.copyWith(isLoading: false, errorMessage: error.message); + } catch (error) { + debugPrint('报名列表请求失败: $error'); + state = state.copyWith(isLoading: false, errorMessage: '报名列表加载失败'); + } + } + + Future requestStreamKey(EventRegistrationItem item) async { + state = state.copyWith( + isRequestingStreamKey: true, + clearErrorMessage: true, + ); + final req = StreamKeyReq( + eventId: item.eventId.isNotEmpty ? item.eventId : _fallbackEventId, + itemId: item.itemId.isNotEmpty ? item.itemId : _fallbackItemId, + userId: item.userId.isNotEmpty ? item.userId : _fallbackStreamUserId, + ); + + try { + final response = await _ref + .read(eventsServerProvider) + .fetchStreamKey(req); + debugPrint('推流 Key 接口响应: $response'); + state = state.copyWith( + isRequestingStreamKey: false, + clearErrorMessage: true, + ); + } on ApiException catch (error) { + debugPrint('推流 Key 接口请求失败: ${error.message}'); + state = state.copyWith( + isRequestingStreamKey: false, + errorMessage: error.message, + ); + } catch (error) { + debugPrint('推流 Key 接口请求失败: $error'); + state = state.copyWith( + isRequestingStreamKey: false, + errorMessage: '推流 Key 获取失败', + ); + } + } +} diff --git a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart index b490635..844f32f 100644 --- a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart +++ b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:recording_tool/app/router/app_navigator.dart'; +import 'package:recording_tool/features/events/pages/page_event_info.dart'; import 'package:recording_tool/features/recording/model/model_recording_context.dart'; -import 'package:recording_tool/features/recording/pages/page_record.dart'; import 'package:recording_tool/shared/widgets/app_bar.dart'; import 'package:recording_tool/shared/widgets/app_button.dart'; -import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart'; class ScanQrCodePage extends StatefulWidget { const ScanQrCodePage({super.key}); @@ -50,21 +49,16 @@ class _AuthPageWidgetState extends State { child: AppButton( label: '扫码', onPressed: () async { - final result = await AppQrScannerDialog.show(context); - if (result == null || result.isEmpty) return; - debugPrint('扫码结果: $result'); - Future.delayed(const Duration(milliseconds: 1)).then((_) { - AppNavigator.push( - const RecordingPage( - recordingContext: ScanQrCodePage.mockRecordingContext, - streamUrl: ScanQrCodePage.mockRtmpUrl, - ), - ); - // AppNavigator.push(PushSteamTestWidget(rtmpUrl: result)); - // AppNavigator.push( - // const WebviewPage(url: 'https://www.dronex.cc/'), - // ); - }); + // final String? playerId = await AppQrScannerDialog.show( + // context, + // ); + // if (playerId == null || playerId.isEmpty) return; + // debugPrint('扫码结果: $playerId'); + + /// TODO + AppNavigator.push( + EventInfoPage(playerId: '74300708945530955'), + ); }, variant: AppButtonVariant.secondary, ), diff --git a/lib/shared/widgets/app_qr_scanner_dialog.dart b/lib/shared/widgets/app_qr_scanner_dialog.dart index dde2fbe..fe91094 100644 --- a/lib/shared/widgets/app_qr_scanner_dialog.dart +++ b/lib/shared/widgets/app_qr_scanner_dialog.dart @@ -59,6 +59,9 @@ class _AppQrScannerDialogState extends State if (_hasScanned || capture.barcodes.isEmpty) return; final value = capture.barcodes.first.rawValue; + print('value:$value'); + return; + if (value == null || value.isEmpty) return; _hasScanned = true;