diff --git a/lib/app/config/api_common.dart b/lib/app/config/api_common.dart index a5cc3f6..0f52718 100644 --- a/lib/app/config/api_common.dart +++ b/lib/app/config/api_common.dart @@ -5,6 +5,9 @@ enum AuthApi { /// 获取推流地址 getStreamKey('/api/events/device/stream/key'), + /// 根据赛事目录获取视频列表 + getRecordList('/api/files'), + /// 获取选手的赛事信息 playerRegistrationList('/api/events/device/player/registration/list'); diff --git a/lib/features/auth/model/model_auth.dart b/lib/features/auth/model/model_auth.dart index db0172a..9ff9118 100644 --- a/lib/features/auth/model/model_auth.dart +++ b/lib/features/auth/model/model_auth.dart @@ -14,3 +14,68 @@ class GetTokenResModel { return {'deviceAccessToken': deviceAccessToken}; } } + +/// 查看录像响应模型 +class GetRecordListResModel { + String? path; + List? items; + + GetRecordListResModel({this.path, this.items}); + + factory GetRecordListResModel.fromJson(Map json) => + GetRecordListResModel( + path: json['path'], + items: json['items'] == null + ? [] + : List.from( + json['items']!.map((x) => RecordListItem.fromJson(x)), + ), + ); + + Map toJson() => { + 'path': path, + 'items': items == null + ? [] + : List.from(items!.map((x) => x.toJson())), + }; +} + +class RecordListItem { + String? name; + String? path; + String? type; + int? size; + DateTime? modTime; + String? url; + String? extension; + + RecordListItem({ + this.name, + this.path, + this.type, + this.size, + this.modTime, + this.url, + this.extension, + }); + + factory RecordListItem.fromJson(Map json) => RecordListItem( + name: json['name'] ?? '', + path: json['path'] ?? '', + type: json['type'] ?? '', + size: json['size'] ?? 0, + modTime: json['modTime'] == null ? null : DateTime.parse(json['modTime']), + url: json['url'] ?? '', + extension: json['extension'] ?? '', + ); + + Map toJson() => { + 'name': name, + 'path': path, + 'type': type, + 'size': size, + 'modTime': modTime?.toIso8601String(), + 'url': url, + 'extension': extension, + }; +} diff --git a/lib/features/auth/model/model_jwt.dart b/lib/features/auth/model/model_jwt.dart new file mode 100644 index 0000000..1388741 --- /dev/null +++ b/lib/features/auth/model/model_jwt.dart @@ -0,0 +1,56 @@ +// To parse this JSON data, do +// +// final jwtDecodedData = jwtDecodedDataFromJson(jsonString); + +import 'dart:convert'; + +JwtDecodedData jwtDecodedDataFromJson(String str) => + JwtDecodedData.fromJson(json.decode(str)); + +String jwtDecodedDataToJson(JwtDecodedData data) => json.encode(data.toJson()); + +class JwtDecodedData { + String? authType; + String? deviceCode; + int? deviceId; + String? deviceRole; + String? eventName; + double? oId; + List? oIds; + double? organizerId; + + JwtDecodedData({ + this.authType, + this.deviceCode, + this.deviceId, + this.deviceRole, + this.eventName, + this.oId, + this.oIds, + this.organizerId, + }); + + factory JwtDecodedData.fromJson(Map json) => JwtDecodedData( + authType: json["authType"], + deviceCode: json["deviceCode"], + deviceId: json["deviceId"], + deviceRole: json["deviceRole"], + eventName: json["eventName"], + oId: json["oId"]?.toDouble(), + oIds: json["oIds"] == null + ? [] + : List.from(json["oIds"]!.map((x) => x?.toDouble())), + organizerId: json["organizerId"]?.toDouble(), + ); + + Map toJson() => { + "authType": authType, + "deviceCode": deviceCode, + "deviceId": deviceId, + "deviceRole": deviceRole, + "eventName": eventName, + "oId": oId, + "oIds": oIds == null ? [] : List.from(oIds!.map((x) => x)), + "organizerId": organizerId, + }; +} diff --git a/lib/features/auth/pages/page_auth.dart b/lib/features/auth/pages/page_auth.dart index 9e3d184..dd04216 100644 --- a/lib/features/auth/pages/page_auth.dart +++ b/lib/features/auth/pages/page_auth.dart @@ -24,7 +24,7 @@ class _AuthPageWidgetState extends ConsumerState { _controller = TextEditingController(); _controller?.text = '555'; - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { final token = AppStorage.getString(StorageKeys.authToken); if (token?.isNotEmpty ?? false) { AppNavigator.pushReplacement(const ScanQrCodePage()); diff --git a/lib/features/auth/server/server_auth.dart b/lib/features/auth/server/server_auth.dart index 20679ba..2c3ad90 100644 --- a/lib/features/auth/server/server_auth.dart +++ b/lib/features/auth/server/server_auth.dart @@ -1,7 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:recording_tool/app/config/api_common.dart'; -import 'package:recording_tool/core/cache/app_storage.dart'; -import 'package:recording_tool/core/cache/storage_keys.dart'; import 'package:recording_tool/core/network/providers/dio_providers.dart'; import 'package:recording_tool/core/utils/device_utils.dart'; import 'package:recording_tool/features/auth/model/model_auth.dart'; @@ -25,7 +23,22 @@ class AuthServer { throw const FormatException('登录响应缺少 TOKEN'); } - await AppStorage.setString(StorageKeys.authToken, data.deviceAccessToken); + return data; + } + + /// 获取赛事列表 + /// [path] 赛事目录 + static Future getRecordList( + Ref ref, + String path, + ) async { + final apiClient = ref.read(apiClientProvider); + final data = await apiClient.get( + 'http://sheling.local:9001/${AuthApi.getRecordList.path}', + queryParameters: {'path': path}, + parser: (json) => + GetRecordListResModel.fromJson(json as Map), + ); return data; } } diff --git a/lib/features/auth/state/state_auth.dart b/lib/features/auth/state/state_auth.dart index 910a48a..31b1251 100644 --- a/lib/features/auth/state/state_auth.dart +++ b/lib/features/auth/state/state_auth.dart @@ -1,13 +1,33 @@ +import 'package:recording_tool/features/auth/model/model_auth.dart'; +import 'package:recording_tool/features/auth/model/model_jwt.dart'; + class AuthState { - const AuthState({this.isLoading = false, this.errorMessage}); + const AuthState({ + this.isLoading = false, + this.errorMessage, + this.jwtDecodedData, + this.recordList, + }); final bool isLoading; final String? errorMessage; - AuthState copyWith({bool? isLoading, String? errorMessage}) { + /// 解析后的 TOKEN 数据 + final JwtDecodedData? jwtDecodedData; + + /// 赛事列表 + final List? recordList; + + AuthState copyWith({ + bool? isLoading, + String? errorMessage, + JwtDecodedData? jwtDecodedData, + List? recordList, + }) { return AuthState( isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage, + errorMessage: errorMessage ?? this.errorMessage, + jwtDecodedData: jwtDecodedData ?? this.jwtDecodedData, ); } } diff --git a/lib/features/auth/view_model_auth/view_model_auth.dart b/lib/features/auth/view_model_auth/view_model_auth.dart index 5054d9d..906a7e2 100644 --- a/lib/features/auth/view_model_auth/view_model_auth.dart +++ b/lib/features/auth/view_model_auth/view_model_auth.dart @@ -1,12 +1,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:recording_tool/core/cache/app_storage.dart'; +import 'package:recording_tool/core/cache/storage_keys.dart'; import 'package:recording_tool/core/network/api_exception.dart'; +import 'package:recording_tool/features/auth/model/model_jwt.dart'; import 'package:recording_tool/features/auth/server/server_auth.dart'; import 'package:recording_tool/features/auth/state/state_auth.dart'; -final authProvider = StateNotifierProvider((ref) { - return AuthViewModel(ref); -}); +final authProvider = + StateNotifierProvider.autoDispose((ref) { + return AuthViewModel(ref); + }); class AuthViewModel extends StateNotifier { AuthViewModel(this._ref) : super(const AuthState()); @@ -22,8 +27,12 @@ class AuthViewModel extends StateNotifier { state = const AuthState(isLoading: true); try { - await AuthServer.login(passCode, _ref); + final data = await AuthServer.login(passCode, _ref); + await AppStorage.setString(StorageKeys.authToken, data.deviceAccessToken); state = const AuthState(); + if (data.deviceAccessToken.isNotEmpty) { + parseTokenSetState(data.deviceAccessToken); + } return true; } on FormatException catch (error) { state = AuthState(errorMessage: error.message); @@ -36,4 +45,24 @@ class AuthViewModel extends StateNotifier { return false; } } + + /// 解析 TOKEN,并更新状态 + Future parseTokenSetState(String token) async { + final decoded = JwtDecoder.decode(token); + if (decoded['data'] != null && decoded['data'] is Map) { + final data = JwtDecodedData.fromJson(decoded['data']); + state = state.copyWith(jwtDecodedData: data); + return true; + } + return false; + } + + /// 获取赛事列表 + Future getRecordList(String eventName) async { + if (eventName.isEmpty) return false; + final data = await AuthServer.getRecordList(_ref, eventName); + if (data.items == null || data.items!.isEmpty) return false; + state = state.copyWith(recordList: data.items); + return true; + } } diff --git a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart index 16e6be4..4a930e2 100644 --- a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart +++ b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart @@ -1,13 +1,19 @@ 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/pages/page_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/recording/model/model_recording_context.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'; +import 'package:recording_tool/shared/widgets/app_toast.dart'; -class ScanQrCodePage extends StatefulWidget { +class ScanQrCodePage extends ConsumerStatefulWidget { const ScanQrCodePage({super.key}); static const mockRtmpUrl = @@ -24,10 +30,28 @@ class ScanQrCodePage extends StatefulWidget { ); @override - State createState() => _AuthPageWidgetState(); + ConsumerState createState() => _AuthPageWidgetState(); } -class _AuthPageWidgetState extends State { +class _AuthPageWidgetState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final token = AppStorage.getString(StorageKeys.authToken); + if (token?.isNotEmpty ?? false) { + final success = await ref + .read(authProvider.notifier) + .parseTokenSetState(token!); + if (!success) { + AppToast.show('请重新鉴权'); + AppNavigator.pushAndRemoveUntil(const AuthPageWidget()); + return; + } + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -44,6 +68,30 @@ class _AuthPageWidgetState extends State { ), SizedBox(height: 20.h), + Consumer( + builder: (context, ref, child) { + return SizedBox( + width: 280.w, + height: 80.h, + child: AppButton( + label: '查看录像', + onPressed: () async { + final data = ref.watch( + authProvider.select((state) => state.jwtDecodedData), + ); + if (data == null) return; + debugPrint('赛事名字: ${data.eventName}'); + final eventName = data.eventName ?? ''; + await ref + .read(authProvider.notifier) + .getRecordList(eventName); + }, + variant: AppButtonVariant.secondary, + ), + ); + }, + ), + SizedBox(height: 16.h), SizedBox( width: 280.w, height: 80.h, diff --git a/pubspec.yaml b/pubspec.yaml index d6a9b4d..fa5f8b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: webview_flutter: ^4.14.0 apivideo_live_stream: path: plugins/apivideo_live_stream + jwt_decoder: ^2.0.1 dev_dependencies: flutter_test: