添加 jwt_decoder 依赖;更新 AuthApi 枚举以支持获取录像列表;新增录像响应模型和相关状态管理;重构 AuthViewModel 以解析和存储 JWT 数据;更新 ScanQrCodePage 以处理用户鉴权和获取录像列表。
This commit is contained in:
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user