添加 jwt_decoder 依赖;更新 AuthApi 枚举以支持获取录像列表;新增录像响应模型和相关状态管理;重构 AuthViewModel 以解析和存储 JWT 数据;更新 ScanQrCodePage 以处理用户鉴权和获取录像列表。

This commit is contained in:
2026-07-14 10:16:31 +08:00
parent 45000b4188
commit 16ad7b253c
9 changed files with 249 additions and 14 deletions
+3
View File
@@ -5,6 +5,9 @@ enum AuthApi {
/// 获取推流地址 /// 获取推流地址
getStreamKey('/api/events/device/stream/key'), getStreamKey('/api/events/device/stream/key'),
/// 根据赛事目录获取视频列表
getRecordList('/api/files'),
/// 获取选手的赛事信息 /// 获取选手的赛事信息
playerRegistrationList('/api/events/device/player/registration/list'); playerRegistrationList('/api/events/device/player/registration/list');
+65
View File
@@ -14,3 +14,68 @@ class GetTokenResModel {
return {'deviceAccessToken': deviceAccessToken}; return {'deviceAccessToken': deviceAccessToken};
} }
} }
/// 查看录像响应模型
class GetRecordListResModel {
String? path;
List<RecordListItem>? items;
GetRecordListResModel({this.path, this.items});
factory GetRecordListResModel.fromJson(Map<String, dynamic> json) =>
GetRecordListResModel(
path: json['path'],
items: json['items'] == null
? []
: List<RecordListItem>.from(
json['items']!.map((x) => RecordListItem.fromJson(x)),
),
);
Map<String, dynamic> toJson() => {
'path': path,
'items': items == null
? []
: List<dynamic>.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<String, dynamic> 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<String, dynamic> toJson() => {
'name': name,
'path': path,
'type': type,
'size': size,
'modTime': modTime?.toIso8601String(),
'url': url,
'extension': extension,
};
}
+56
View File
@@ -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<double>? oIds;
double? organizerId;
JwtDecodedData({
this.authType,
this.deviceCode,
this.deviceId,
this.deviceRole,
this.eventName,
this.oId,
this.oIds,
this.organizerId,
});
factory JwtDecodedData.fromJson(Map<String, dynamic> 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<double>.from(json["oIds"]!.map((x) => x?.toDouble())),
organizerId: json["organizerId"]?.toDouble(),
);
Map<String, dynamic> toJson() => {
"authType": authType,
"deviceCode": deviceCode,
"deviceId": deviceId,
"deviceRole": deviceRole,
"eventName": eventName,
"oId": oId,
"oIds": oIds == null ? [] : List<dynamic>.from(oIds!.map((x) => x)),
"organizerId": organizerId,
};
}
+1 -1
View File
@@ -24,7 +24,7 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
_controller = TextEditingController(); _controller = TextEditingController();
_controller?.text = '555'; _controller?.text = '555';
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) async {
final token = AppStorage.getString(StorageKeys.authToken); final token = AppStorage.getString(StorageKeys.authToken);
if (token?.isNotEmpty ?? false) { if (token?.isNotEmpty ?? false) {
AppNavigator.pushReplacement(const ScanQrCodePage()); AppNavigator.pushReplacement(const ScanQrCodePage());
+16 -3
View File
@@ -1,7 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:recording_tool/app/config/api_common.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/network/providers/dio_providers.dart';
import 'package:recording_tool/core/utils/device_utils.dart'; import 'package:recording_tool/core/utils/device_utils.dart';
import 'package:recording_tool/features/auth/model/model_auth.dart'; import 'package:recording_tool/features/auth/model/model_auth.dart';
@@ -25,7 +23,22 @@ class AuthServer {
throw const FormatException('登录响应缺少 TOKEN'); throw const FormatException('登录响应缺少 TOKEN');
} }
await AppStorage.setString(StorageKeys.authToken, data.deviceAccessToken); return data;
}
/// 获取赛事列表
/// [path] 赛事目录
static Future<GetRecordListResModel> getRecordList(
Ref ref,
String path,
) async {
final apiClient = ref.read(apiClientProvider);
final data = await apiClient.get<GetRecordListResModel>(
'http://sheling.local:9001/${AuthApi.getRecordList.path}',
queryParameters: {'path': path},
parser: (json) =>
GetRecordListResModel.fromJson(json as Map<String, dynamic>),
);
return data; return data;
} }
} }
+23 -3
View File
@@ -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 { class AuthState {
const AuthState({this.isLoading = false, this.errorMessage}); const AuthState({
this.isLoading = false,
this.errorMessage,
this.jwtDecodedData,
this.recordList,
});
final bool isLoading; final bool isLoading;
final String? errorMessage; final String? errorMessage;
AuthState copyWith({bool? isLoading, String? errorMessage}) { /// 解析后的 TOKEN 数据
final JwtDecodedData? jwtDecodedData;
/// 赛事列表
final List<RecordListItem>? recordList;
AuthState copyWith({
bool? isLoading,
String? errorMessage,
JwtDecodedData? jwtDecodedData,
List<RecordListItem>? recordList,
}) {
return AuthState( return AuthState(
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage, errorMessage: errorMessage ?? this.errorMessage,
jwtDecodedData: jwtDecodedData ?? this.jwtDecodedData,
); );
} }
} }
@@ -1,12 +1,17 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.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/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/server/server_auth.dart';
import 'package:recording_tool/features/auth/state/state_auth.dart'; import 'package:recording_tool/features/auth/state/state_auth.dart';
final authProvider = StateNotifierProvider<AuthViewModel, AuthState>((ref) { final authProvider =
StateNotifierProvider.autoDispose<AuthViewModel, AuthState>((ref) {
return AuthViewModel(ref); return AuthViewModel(ref);
}); });
class AuthViewModel extends StateNotifier<AuthState> { class AuthViewModel extends StateNotifier<AuthState> {
AuthViewModel(this._ref) : super(const AuthState()); AuthViewModel(this._ref) : super(const AuthState());
@@ -22,8 +27,12 @@ class AuthViewModel extends StateNotifier<AuthState> {
state = const AuthState(isLoading: true); state = const AuthState(isLoading: true);
try { try {
await AuthServer.login(passCode, _ref); final data = await AuthServer.login(passCode, _ref);
await AppStorage.setString(StorageKeys.authToken, data.deviceAccessToken);
state = const AuthState(); state = const AuthState();
if (data.deviceAccessToken.isNotEmpty) {
parseTokenSetState(data.deviceAccessToken);
}
return true; return true;
} on FormatException catch (error) { } on FormatException catch (error) {
state = AuthState(errorMessage: error.message); state = AuthState(errorMessage: error.message);
@@ -36,4 +45,24 @@ class AuthViewModel extends StateNotifier<AuthState> {
return false; return false;
} }
} }
/// 解析 TOKEN,并更新状态
Future<bool> parseTokenSetState(String token) async {
final decoded = JwtDecoder.decode(token);
if (decoded['data'] != null && decoded['data'] is Map<String, dynamic>) {
final data = JwtDecodedData.fromJson(decoded['data']);
state = state.copyWith(jwtDecodedData: data);
return true;
}
return false;
}
/// 获取赛事列表
Future<bool> 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;
}
} }
@@ -1,13 +1,19 @@
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/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/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/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'; 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}); const ScanQrCodePage({super.key});
static const mockRtmpUrl = static const mockRtmpUrl =
@@ -24,10 +30,28 @@ class ScanQrCodePage extends StatefulWidget {
); );
@override @override
State<ScanQrCodePage> createState() => _AuthPageWidgetState(); ConsumerState<ScanQrCodePage> createState() => _AuthPageWidgetState();
} }
class _AuthPageWidgetState extends State<ScanQrCodePage> { class _AuthPageWidgetState extends ConsumerState<ScanQrCodePage> {
@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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -44,6 +68,30 @@ class _AuthPageWidgetState extends State<ScanQrCodePage> {
), ),
SizedBox(height: 20.h), 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( SizedBox(
width: 280.w, width: 280.w,
height: 80.h, height: 80.h,
+1
View File
@@ -53,6 +53,7 @@ dependencies:
webview_flutter: ^4.14.0 webview_flutter: ^4.14.0
apivideo_live_stream: apivideo_live_stream:
path: plugins/apivideo_live_stream path: plugins/apivideo_live_stream
jwt_decoder: ^2.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: