Merge branch 'linfeng/drone_scoring/dev_1.1.0/2026630' into linfeng/drone_video/dev_1.1.0/20260707

This commit is contained in:
2026-07-29 09:19:19 +08:00
21 changed files with 1041 additions and 396 deletions
+22 -9
View File
@@ -22,15 +22,20 @@ class GetRecordListResModel {
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)),
),
);
factory GetRecordListResModel.fromJson(Map<String, dynamic> json) {
final rawItems = json['items'];
return GetRecordListResModel(
path: json['path']?.toString(),
items: rawItems is! List
? []
: rawItems
.whereType<Map>()
.map(
(x) => RecordListItem.fromJson(Map<String, dynamic>.from(x)),
)
.toList(),
);
}
Map<String, dynamic> toJson() => {
'path': path,
@@ -40,6 +45,14 @@ class GetRecordListResModel {
};
}
enum RecordItemType {
file('file'),
directory('dir');
final String value;
const RecordItemType(this.value);
}
class RecordListItem {
String? name;
String? path;
+4
View File
@@ -18,6 +18,7 @@ class JwtDecodedData {
String? oId;
List<String>? oIds;
String? organizerId;
String? nasIpAddr;
JwtDecodedData({
this.authType,
@@ -28,6 +29,7 @@ class JwtDecodedData {
this.oId,
this.oIds,
this.organizerId,
this.nasIpAddr,
});
factory JwtDecodedData.fromJson(Map<String, dynamic> json) => JwtDecodedData(
@@ -41,6 +43,7 @@ class JwtDecodedData {
? []
: List<String>.from(json['oIds']!.map((x) => x?.toString())),
organizerId: json['organizerId']?.toString(),
nasIpAddr: json['nasIpAddr']?.toString(),
);
Map<String, dynamic> toJson() => {
@@ -52,6 +55,7 @@ class JwtDecodedData {
'oId': oId,
'oIds': oIds == null ? [] : List<dynamic>.from(oIds!.map((x) => x)),
'organizerId': organizerId,
'nasIpAddr': nasIpAddr,
};
}
+40 -21
View File
@@ -12,7 +12,6 @@ import 'package:recording_tool/core/utils/util_search_nasIp.dart';
import 'package:recording_tool/features/auth/view_model_auth/view_model_auth.dart';
import 'package:recording_tool/features/scan_qrcode/pages/page_scan_qrcode.dart';
import 'package:recording_tool/gen/assets.gen.dart';
import 'package:recording_tool/shared/widgets/app_button.dart';
import 'package:recording_tool/shared/widgets/app_dialog.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart';
@@ -26,10 +25,12 @@ class AuthPageWidget extends ConsumerStatefulWidget {
class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
late final TextEditingController _controller;
/// 记录点击次数
int _clickCount = 0;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: '999779');
_controller = TextEditingController(text: '');
WidgetsBinding.instance.addPostFrameCallback((_) async {
// 静默探测 NAS,不阻塞登录 / 自动跳转
@@ -37,6 +38,12 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
final token = AppStorage.getString(StorageKeys.authToken);
if (token?.isNotEmpty ?? false) {
final success = await ref
.read(authProvider.notifier)
.parseTokenSetState();
if (!success) {
return;
}
AppNavigator.push(const ScanQrCodePage());
}
});
@@ -48,6 +55,12 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
super.dispose();
}
void _showDeviceCodeDialog() async {
final deviceCode = await DeviceUtils.deviceCode();
if (!mounted) return;
AppDialog.confirm(context, title: '设备码:$deviceCode');
}
@override
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
@@ -61,14 +74,11 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
backgroundColor: Colors.white,
body: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
Positioned.fill(
child: Image.asset(
_AuthAssets.pageBg,
width: double.infinity,
fit: BoxFit.fitWidth,
fit: BoxFit.fill,
),
),
SafeArea(
@@ -80,11 +90,20 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
SizedBox(height: 190.h),
ClipRRect(
borderRadius: BorderRadius.circular(24.r),
child: Image.asset(
_AuthAssets.appIcon,
width: 82.w,
height: 82.w,
fit: BoxFit.cover,
child: GestureDetector(
onTap: () {
_clickCount++;
if (_clickCount >= 5) {
_showDeviceCodeDialog();
_clickCount = 0;
}
},
child: Image.asset(
_AuthAssets.appIcon,
width: 82.w,
height: 82.w,
fit: BoxFit.cover,
),
),
),
SizedBox(height: 18.h),
@@ -101,15 +120,15 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
isLoading: authState.isLoading,
onPressed: _handleSubmit,
),
SizedBox(height: 20.h),
AppButton(
onPressed: () async {
final deviceCode = await DeviceUtils.deviceCode();
if (!mounted) return;
AppDialog.confirm(context, title: '设备码:$deviceCode');
},
label: '获取设备码',
),
// SizedBox(height: 20.h),
// AppButton(
// onPressed: () async {
// final deviceCode = await DeviceUtils.deviceCode();
// if (!mounted) return;
// AppDialog.confirm(context, title: '设备码:$deviceCode');
// },
// label: '获取设备码',
// ),
],
),
),
+44 -9
View File
@@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:recording_tool/app/config/api_common.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';
import 'package:recording_tool/features/auth/model/model_jwt.dart';
class AuthServer {
/// [passCode] 口令码
@@ -28,17 +30,50 @@ class AuthServer {
/// 获取赛事列表
/// [path] 赛事目录
static Future<GetRecordListResModel> getRecordList(
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;
try {
final apiClient = ref.read(apiClientProvider);
// NAS /api/files 直接返回 {path, items},不是业务网关的 {code,message,data} 包装。
final data = await apiClient.get<GetRecordListResModel>(
'http://sheling.local:9001/${AuthApi.getRecordList.path}',
queryParameters: {'path': path},
wrapResponse: false,
parser: (json) {
if (json is! Map) {
throw const FormatException('录像列表响应格式错误');
}
return GetRecordListResModel.fromJson(
Map<String, dynamic>.from(json),
);
},
);
return data;
} catch (error) {
debugPrint('getRecordList failed: $error');
return null;
}
}
/// 获取登录设备信息
static Future<JwtDecodedData?> getLoginDeviceInfo(Ref ref) async {
try {
final apiClient = ref.read(apiClientProvider);
final data = await apiClient.get<JwtDecodedData>(
AuthApi.getLoginDeviceInfo.path,
parser: (json) {
if (json is! Map) {
throw const FormatException('登录设备信息响应格式错误');
}
return JwtDecodedData.fromJson(Map<String, dynamic>.from(json));
},
);
return data;
} catch (error) {
debugPrint('getLoginDeviceInfo failed: $error');
return null;
}
}
}
+1
View File
@@ -28,6 +28,7 @@ class AuthState {
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
jwtDecodedData: jwtDecodedData ?? this.jwtDecodedData,
recordList: recordList ?? this.recordList,
);
}
}
@@ -1,11 +1,9 @@
import 'package:flutter/foundation.dart';
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/model/model_auth.dart';
import 'package:recording_tool/features/auth/server/server_auth.dart';
import 'package:recording_tool/features/auth/state/state_auth.dart';
@@ -32,7 +30,11 @@ class AuthViewModel extends StateNotifier<AuthState> {
await AppStorage.setString(StorageKeys.authToken, data.deviceAccessToken);
state = const AuthState();
if (data.deviceAccessToken.isNotEmpty) {
parseTokenSetState(data.deviceAccessToken);
final ok = await parseTokenSetState();
if (!ok) {
state = const AuthState(errorMessage: '获取赛事信息失败,请重试');
return false;
}
}
return true;
} on FormatException catch (error) {
@@ -48,29 +50,41 @@ class AuthViewModel extends StateNotifier<AuthState> {
}
/// 解析 TOKEN,并更新状态
Future<bool> parseTokenSetState(String token) async {
final decoded = JwtDecoder.decode(token);
final rawData = decoded['data'];
if (rawData is! Map) return false;
try {
final data = JwtDecodedData.fromJson(Map<String, dynamic>.from(rawData));
state = state.copyWith(jwtDecodedData: data);
return true;
} catch (error) {
debugPrint('认证失败,请重试: $error');
return false;
}
Future<bool> parseTokenSetState() async {
final data = await AuthServer.getLoginDeviceInfo(_ref);
if (data == null) return false;
state = state.copyWith(jwtDecodedData: data);
return true;
// final decoded = JwtDecoder.decode(token);
// final rawData = decoded['data'];
// if (rawData is! Map) return false;
// try {
// final data = JwtDecodedData.fromJson(Map<String, dynamic>.from(rawData));
// state = state.copyWith(jwtDecodedData: data);
// return true;
// } catch (error) {
// debugPrint('认证失败,请重试: $error');
// 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;
if (data == null) return false;
if (data.items == null) return false;
state = state.copyWith(recordList: data.items);
return true;
}
/// 获取指定目录的录像列表(不更新 state,用于目录下钻)
Future<List<RecordListItem>?> fetchRecordList(String path) async {
if (path.isEmpty) return null;
final data = await AuthServer.getRecordList(_ref, path);
return data?.items;
}
/// 清空授权信息(本地 token + 内存状态)
Future<void> clearAuth() async {
await AppStorage.remove(StorageKeys.authToken);