更新 devtools_options.yaml 以启用 riverpod 和 shared_preferences 扩展;更新 AuthApi 枚举以添加 playerRegistrationList API;重构 Auth 页面以在用户已登录时直接导航到扫码页面;重构 EventInfoPage 以使用新的状态管理和加载赛事报名信息;更新扫码页面以导航到 EventInfoPage。
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -103,10 +103,7 @@ class AppNavigator {
|
||||
}
|
||||
|
||||
static void pop<T extends Object?>({BuildContext? context, T? result}) {
|
||||
Navigator.of(
|
||||
context ?? AppNavigator.context!,
|
||||
rootNavigator: true,
|
||||
).pop<T>(result);
|
||||
Navigator.maybePop(context ?? AppNavigator.context!);
|
||||
}
|
||||
|
||||
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_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<AuthPageWidget> {
|
||||
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<AuthPageWidget> {
|
||||
.auth(code ?? '');
|
||||
if (!mounted) return;
|
||||
if (success) {
|
||||
AppNavigator.push(const EventInfoPage());
|
||||
AppNavigator.push(const ScanQrCodePage());
|
||||
return;
|
||||
}
|
||||
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_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<EventInfoPage> 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<EventInfoPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
ref.read(eventInfoProvider.notifier).loadRegistrationList();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> 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<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 {
|
||||
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) {
|
||||
|
||||
@@ -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_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<ScanQrCodePage> {
|
||||
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,
|
||||
),
|
||||
|
||||
@@ -59,6 +59,9 @@ class _AppQrScannerDialogState extends State<AppQrScannerDialog>
|
||||
if (_hasScanned || capture.barcodes.isEmpty) return;
|
||||
|
||||
final value = capture.barcodes.first.rawValue;
|
||||
print('value:$value');
|
||||
return;
|
||||
|
||||
if (value == null || value.isEmpty) return;
|
||||
|
||||
_hasScanned = true;
|
||||
|
||||
Reference in New Issue
Block a user