更新 devtools_options.yaml 以启用 riverpod 和 shared_preferences 扩展;更新 AuthApi 枚举以添加 playerRegistrationList API;重构 Auth 页面以在用户已登录时直接导航到扫码页面;重构 EventInfoPage 以使用新的状态管理和加载赛事报名信息;更新扫码页面以导航到 EventInfoPage。
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
description: This file stores settings for Dart & Flutter DevTools.
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
extensions:
|
extensions:
|
||||||
|
- riverpod: true
|
||||||
|
- shared_preferences: true
|
||||||
@@ -3,7 +3,10 @@ enum AuthApi {
|
|||||||
getToken('/api/events/device/token'),
|
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;
|
final String path;
|
||||||
const AuthApi(this.path);
|
const AuthApi(this.path);
|
||||||
|
|||||||
@@ -103,10 +103,7 @@ class AppNavigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void pop<T extends Object?>({BuildContext? context, T? result}) {
|
static void pop<T extends Object?>({BuildContext? context, T? result}) {
|
||||||
Navigator.of(
|
Navigator.maybePop(context ?? AppNavigator.context!);
|
||||||
context ?? AppNavigator.context!,
|
|
||||||
rootNavigator: true,
|
|
||||||
).pop<T>(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void popTimes({BuildContext? context, int count = 1}) {
|
static void popTimes({BuildContext? context, int count = 1}) {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:recording_tool/app/router/app_navigator.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/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';
|
import 'package:recording_tool/shared/widgets/widgets.dart';
|
||||||
|
|
||||||
class AuthPageWidget extends ConsumerStatefulWidget {
|
class AuthPageWidget extends ConsumerStatefulWidget {
|
||||||
@@ -21,6 +23,13 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_controller = TextEditingController();
|
_controller = TextEditingController();
|
||||||
_controller?.text = '555';
|
_controller?.text = '555';
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final token = AppStorage.getString(StorageKeys.authToken);
|
||||||
|
if (token?.isNotEmpty ?? false) {
|
||||||
|
AppNavigator.pushReplacement(const ScanQrCodePage());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -62,7 +71,7 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
|
|||||||
.auth(code ?? '');
|
.auth(code ?? '');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (success) {
|
if (success) {
|
||||||
AppNavigator.push(const EventInfoPage());
|
AppNavigator.push(const ScanQrCodePage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final message = ref.read(authProvider).errorMessage;
|
final message = ref.read(authProvider).errorMessage;
|
||||||
|
|||||||
@@ -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<String, dynamic> 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<String, dynamic> 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<EventRegistrationItem> 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>()
|
||||||
|
.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<String, dynamic> rawData;
|
||||||
|
|
||||||
|
factory EventRegistrationItem.fromJson(
|
||||||
|
Map<dynamic, dynamic> json, {
|
||||||
|
String userId = '',
|
||||||
|
String playerName = '',
|
||||||
|
}) {
|
||||||
|
final map = Map<String, dynamic>.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<String, dynamic> rawData;
|
||||||
|
|
||||||
|
factory StreamKeyResponse.fromJson(dynamic json) {
|
||||||
|
if (json is Map) {
|
||||||
|
return StreamKeyResponse(rawData: Map<String, dynamic>.from(json));
|
||||||
|
}
|
||||||
|
return StreamKeyResponse(rawData: {'value': json});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => rawData.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<dynamic> _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<String, dynamic> _extractObject(dynamic json) {
|
||||||
|
if (json is Map) {
|
||||||
|
final map = Map<String, dynamic>.from(json);
|
||||||
|
final data = map['data'];
|
||||||
|
if (data is Map) return Map<String, dynamic>.from(data);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
return const {};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _readString(
|
||||||
|
Map<String, dynamic> map,
|
||||||
|
List<String> keys, {
|
||||||
|
String fallback = '',
|
||||||
|
}) {
|
||||||
|
return _readNullableString(map, keys) ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _readScheduleTime(
|
||||||
|
Map<String, dynamic> 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<String, dynamic> map, List<String> 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<String, dynamic> map, List<String> 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<String, dynamic> map, List<String> 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;
|
||||||
|
}
|
||||||
@@ -1,64 +1,51 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:recording_tool/app/router/app_navigator.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/features/recording/pages/page_record.dart';
|
||||||
|
import 'package:recording_tool/shared/widgets/widgets.dart';
|
||||||
|
|
||||||
class EventInfoPage extends StatelessWidget {
|
class EventInfoPage extends ConsumerStatefulWidget {
|
||||||
const EventInfoPage({super.key});
|
const EventInfoPage({super.key, required this.playerId});
|
||||||
|
final String playerId;
|
||||||
static const mockRtmpUrl =
|
static const mockRtmpUrl =
|
||||||
'rtmp://192.168.1.245:19090/蔡依婷vs夏志豪_空中格斗赛_高中组/蔡依婷vs夏志豪_空中格斗赛_高中组';
|
'rtmp://192.168.1.245:19090/蔡依婷vs夏志豪_空中格斗赛_高中组/蔡依婷vs夏志豪_空中格斗赛_高中组';
|
||||||
|
|
||||||
static const _profile = EventProfile(
|
@override
|
||||||
avatarLabel: '头像',
|
ConsumerState<EventInfoPage> createState() => _EventInfoPageState();
|
||||||
name: '王东方',
|
}
|
||||||
phone: '199966666527',
|
|
||||||
eventTitle: '全国青少年无人机大赛',
|
|
||||||
);
|
|
||||||
|
|
||||||
static const _items = [
|
class _EventInfoPageState extends ConsumerState<EventInfoPage> {
|
||||||
EventScheduleItem(
|
@override
|
||||||
name: '个人飞行赛',
|
void initState() {
|
||||||
group: '小学组',
|
super.initState();
|
||||||
venue: '场地 1',
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
time: '7月1日 12:00-15:00',
|
if (!mounted) return;
|
||||||
laneNo: '10号',
|
ref.read(eventInfoProvider.notifier).loadRegistrationList();
|
||||||
),
|
});
|
||||||
EventScheduleItem(
|
}
|
||||||
name: '空中足球赛',
|
|
||||||
group: '小学组',
|
Future<void> onItemTap(EventRegistrationItem item) async {
|
||||||
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) {
|
|
||||||
debugPrint('item tapped: ${item.name}');
|
debugPrint('item tapped: ${item.name}');
|
||||||
|
await ref.read(eventInfoProvider.notifier).requestStreamKey(item);
|
||||||
|
if (!mounted) return;
|
||||||
|
final profile = ref.read(eventInfoProvider).profile;
|
||||||
AppNavigator.push(
|
AppNavigator.push(
|
||||||
RecordingPage(
|
RecordingPage(
|
||||||
recordingContext: item.toRecordingContext(profile: _profile),
|
recordingContext: item.toRecordingContext(profile: profile),
|
||||||
streamUrl: mockRtmpUrl,
|
streamUrl: EventInfoPage.mockRtmpUrl,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final state = ref.watch(eventInfoProvider);
|
||||||
|
final profile = state.profile;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
@@ -71,10 +58,10 @@ class EventInfoPage extends StatelessWidget {
|
|||||||
padding: EdgeInsets.fromLTRB(62.w, 8.h, 62.w, 40.h),
|
padding: EdgeInsets.fromLTRB(62.w, 8.h, 62.w, 40.h),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const _ProfileSection(profile: _profile),
|
_ProfileSection(profile: profile),
|
||||||
SizedBox(height: 82.h),
|
SizedBox(height: 20.h),
|
||||||
Text(
|
Text(
|
||||||
_profile.eventTitle,
|
state.eventTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
@@ -85,12 +72,12 @@ class EventInfoPage extends StatelessWidget {
|
|||||||
SizedBox(height: 24.h),
|
SizedBox(height: 24.h),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 400.h,
|
height: 400.h,
|
||||||
child: ListView.builder(
|
child: _ScheduleList(
|
||||||
itemCount: _items.length,
|
state: state,
|
||||||
itemBuilder: (context, index) => _ScheduleCard(
|
onRetry: () => ref
|
||||||
item: _items[index],
|
.read(eventInfoProvider.notifier)
|
||||||
onTap: () => onItemTap(_items[index]),
|
.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 {
|
class _Header extends StatelessWidget {
|
||||||
const _Header({required this.onBack});
|
const _Header({required this.onBack});
|
||||||
|
|
||||||
@@ -184,18 +125,10 @@ class _ProfileSection extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
profile.avatarUrl.isEmpty
|
||||||
// width: 225.w,
|
? AppAvatar(size: 50.r)
|
||||||
// height: 184.h,
|
: AppAvatar(size: 50.r, imageUrl: profile.avatarUrl),
|
||||||
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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 28.w),
|
SizedBox(width: 28.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
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<EventRegistrationItem> 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 {
|
class _ScheduleCard extends StatelessWidget {
|
||||||
const _ScheduleCard({required this.item, required this.onTap});
|
const _ScheduleCard({required this.item, required this.onTap});
|
||||||
|
|
||||||
final EventScheduleItem item;
|
final EventRegistrationItem item;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -259,7 +236,9 @@ class _ScheduleCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${item.name} (${item.group})',
|
item.group.isEmpty
|
||||||
|
? item.name
|
||||||
|
: '${item.name} (${item.group})',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -314,7 +293,7 @@ class _ScheduleCard extends StatelessWidget {
|
|||||||
class _ScheduleBadge extends StatelessWidget {
|
class _ScheduleBadge extends StatelessWidget {
|
||||||
const _ScheduleBadge({required this.item});
|
const _ScheduleBadge({required this.item});
|
||||||
|
|
||||||
final EventScheduleItem item;
|
final EventRegistrationItem item;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -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<EventsServer>((ref) {
|
||||||
|
return EventsServer(ref.watch(apiClientProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
class EventsServer {
|
||||||
|
const EventsServer(this._apiClient);
|
||||||
|
|
||||||
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
|
Future<EventRegistrationList> fetchPlayerRegistrationList(
|
||||||
|
PlayerRegistrationListReq req,
|
||||||
|
) {
|
||||||
|
return _apiClient.request<EventRegistrationList>(
|
||||||
|
AuthApi.playerRegistrationList.path,
|
||||||
|
method: HttpMethod.get,
|
||||||
|
data: FormData.fromMap(req.toFormDataMap()),
|
||||||
|
parser: EventRegistrationList.fromJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StreamKeyResponse> fetchStreamKey(StreamKeyReq req) {
|
||||||
|
return _apiClient.request<StreamKeyResponse>(
|
||||||
|
AuthApi.getStreamKey.path,
|
||||||
|
method: HttpMethod.get,
|
||||||
|
data: FormData.fromMap(req.toFormDataMap()),
|
||||||
|
parser: StreamKeyResponse.fromJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<EventRegistrationItem> 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<EventRegistrationItem>? 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<EventInfoViewModel, EventInfoState>((ref) {
|
||||||
|
return EventInfoViewModel(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
class EventInfoViewModel extends StateNotifier<EventInfoState> {
|
||||||
|
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<void> 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<void> 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 获取失败',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:recording_tool/app/router/app_navigator.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/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_bar.dart';
|
||||||
import 'package:recording_tool/shared/widgets/app_button.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 {
|
class ScanQrCodePage extends StatefulWidget {
|
||||||
const ScanQrCodePage({super.key});
|
const ScanQrCodePage({super.key});
|
||||||
@@ -50,21 +49,16 @@ class _AuthPageWidgetState extends State<ScanQrCodePage> {
|
|||||||
child: AppButton(
|
child: AppButton(
|
||||||
label: '扫码',
|
label: '扫码',
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await AppQrScannerDialog.show(context);
|
// final String? playerId = await AppQrScannerDialog.show(
|
||||||
if (result == null || result.isEmpty) return;
|
// context,
|
||||||
debugPrint('扫码结果: $result');
|
// );
|
||||||
Future.delayed(const Duration(milliseconds: 1)).then((_) {
|
// if (playerId == null || playerId.isEmpty) return;
|
||||||
AppNavigator.push(
|
// debugPrint('扫码结果: $playerId');
|
||||||
const RecordingPage(
|
|
||||||
recordingContext: ScanQrCodePage.mockRecordingContext,
|
/// TODO
|
||||||
streamUrl: ScanQrCodePage.mockRtmpUrl,
|
AppNavigator.push(
|
||||||
),
|
EventInfoPage(playerId: '74300708945530955'),
|
||||||
);
|
);
|
||||||
// AppNavigator.push(PushSteamTestWidget(rtmpUrl: result));
|
|
||||||
// AppNavigator.push(
|
|
||||||
// const WebviewPage(url: 'https://www.dronex.cc/'),
|
|
||||||
// );
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
variant: AppButtonVariant.secondary,
|
variant: AppButtonVariant.secondary,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ class _AppQrScannerDialogState extends State<AppQrScannerDialog>
|
|||||||
if (_hasScanned || capture.barcodes.isEmpty) return;
|
if (_hasScanned || capture.barcodes.isEmpty) return;
|
||||||
|
|
||||||
final value = capture.barcodes.first.rawValue;
|
final value = capture.barcodes.first.rawValue;
|
||||||
|
print('value:$value');
|
||||||
|
return;
|
||||||
|
|
||||||
if (value == null || value.isEmpty) return;
|
if (value == null || value.isEmpty) return;
|
||||||
|
|
||||||
_hasScanned = true;
|
_hasScanned = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user