diff --git a/lib/core/utils/util_search_nasIp.dart b/lib/core/utils/util_search_nasIp.dart new file mode 100644 index 0000000..14770d1 --- /dev/null +++ b/lib/core/utils/util_search_nasIp.dart @@ -0,0 +1,142 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:network_info_plus/network_info_plus.dart'; + +/// 局域网 NAS IP 探测(结果仅存内存静态字段)。 +class UtilSearchNasIp { + UtilSearchNasIp._(); + + static const String _defaultPreUrl = 'http://192.168.1.'; + static const String _probePath = + ':5666/sac/rpcproxy/v1/new-user-guide/status'; + static const Duration _timeout = Duration(seconds: 1); + + /// 命中的 NAS IPv4(仅内存,进程内有效)。 + static String? nasIp; + + /// 并发扫同网段 1~255,超时 1s;首个有 HTTP 响应者写入 [nasIp] 并返回。 + static Future discover() async { + if (nasIp != null && nasIp!.isNotEmpty) { + return nasIp; + } + + final startedAt = DateTime.now(); + final preUrl = await _resolvePreUrl(); + final dio = Dio( + BaseOptions( + connectTimeout: _timeout, + receiveTimeout: _timeout, + sendTimeout: _timeout, + validateStatus: (_) => true, + ), + ); + final cancelToken = CancelToken(); + + try { + await Future.wait([ + for (var host = 1; host <= 255; host++) + _probeHost( + dio: dio, + cancelToken: cancelToken, + preUrl: preUrl, + host: host, + startedAt: startedAt, + ), + ]); + if (nasIp == null) { + final elapsedMs = DateTime.now().difference(startedAt).inMilliseconds; + debugPrint('NAS 探测未命中,总耗时: ${elapsedMs}ms'); + } + return nasIp; + } finally { + dio.close(force: true); + } + } + + /// 由本机 Wi‑Fi IPv4 推导 `http://a.b.c.`,失败则用默认网段。 + static Future _resolvePreUrl() async { + try { + final wifiIp = await NetworkInfo().getWifiIP(); + if (wifiIp == null || wifiIp.isEmpty) return _defaultPreUrl; + final parts = wifiIp.split('.'); + if (parts.length != 4) return _defaultPreUrl; + return 'http://${parts[0]}.${parts[1]}.${parts[2]}.'; + } catch (_) { + return _defaultPreUrl; + } + } + + /// 探测单个主机;任意 HTTP 响应视为命中。 + static Future _probeHost({ + required Dio dio, + required CancelToken cancelToken, + required String preUrl, + required int host, + required DateTime startedAt, + }) async { + if (cancelToken.isCancelled || nasIp != null) return; + + final target = '$preUrl$host$_probePath'; + try { + await dio.get(target, cancelToken: cancelToken); + _onHit(preUrl, host, cancelToken, startedAt); + } on DioException catch (error) { + if (error.type == DioExceptionType.cancel) return; + // 带 response 的 DioException 也表示已连通 + if (error.response != null) { + _onHit(preUrl, host, cancelToken, startedAt); + } + } catch (_) { + // 超时 / 连接失败:未命中 + } + } + + /// 记录命中:写 [nasIp]、打印 IP 与总耗时,并取消其余请求。 + static void _onHit( + String preUrl, + int host, + CancelToken cancelToken, + DateTime startedAt, + ) { + if (nasIp != null) return; + final ip = '${_hostPrefix(preUrl)}$host'; + nasIp = ip; + final elapsedMs = DateTime.now().difference(startedAt).inMilliseconds; + debugPrint('NAS IP: $ip,探测总耗时: ${elapsedMs}ms'); + if (!cancelToken.isCancelled) { + cancelToken.cancel('nas-found'); + } + } + + /// `http://192.168.1.` → `192.168.1.` + static String _hostPrefix(String preUrl) { + return preUrl.replaceFirst(RegExp(r'^https?://'), ''); + } +} + +// --------------------------------------------------------------------------- +// 调用案例 +// --------------------------------------------------------------------------- +// +// 1) Auth 页静默探测(不阻塞登录): +// +// import 'dart:async'; +// import 'package:recording_tool/core/utils/util_search_nasIp.dart'; +// +// WidgetsBinding.instance.addPostFrameCallback((_) { +// unawaited(UtilSearchNasIp.discover()); +// }); +// +// 2) 业务侧读取已命中 IP: +// +// final ip = UtilSearchNasIp.nasIp; +// if (ip != null) { +// final base = 'http://$ip:5666'; +// // 使用 base 访问 NAS... +// } +// +// 3) 主动等待探测结果(少用,会阻塞当前异步流程): +// +// final ip = await UtilSearchNasIp.discover(); +// debugPrint('result: $ip'); +// diff --git a/lib/features/auth/model/model_auth.dart b/lib/features/auth/model/model_auth.dart index 9ff9118..e893150 100644 --- a/lib/features/auth/model/model_auth.dart +++ b/lib/features/auth/model/model_auth.dart @@ -22,15 +22,20 @@ class GetRecordListResModel { 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)), - ), - ); + factory GetRecordListResModel.fromJson(Map json) { + final rawItems = json['items']; + return GetRecordListResModel( + path: json['path']?.toString(), + items: rawItems is! List + ? [] + : rawItems + .whereType() + .map( + (x) => RecordListItem.fromJson(Map.from(x)), + ) + .toList(), + ); + } Map toJson() => { 'path': path, diff --git a/lib/features/auth/pages/page_auth.dart b/lib/features/auth/pages/page_auth.dart index 7e38233..42e8eba 100644 --- a/lib/features/auth/pages/page_auth.dart +++ b/lib/features/auth/pages/page_auth.dart @@ -26,7 +26,7 @@ class _AuthPageWidgetState extends ConsumerState { @override void initState() { super.initState(); - _controller = TextEditingController(text: '999779'); + _controller = TextEditingController(text: '905758'); WidgetsBinding.instance.addPostFrameCallback((_) async { final token = AppStorage.getString(StorageKeys.authToken); diff --git a/lib/features/auth/server/server_auth.dart b/lib/features/auth/server/server_auth.dart index 2c3ad90..2ccf884 100644 --- a/lib/features/auth/server/server_auth.dart +++ b/lib/features/auth/server/server_auth.dart @@ -1,3 +1,4 @@ +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'; @@ -28,17 +29,30 @@ class AuthServer { /// 获取赛事列表 /// [path] 赛事目录 - static Future getRecordList( + 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; + try { + final apiClient = ref.read(apiClientProvider); + // NAS /api/files 直接返回 {path, items},不是业务网关的 {code,message,data} 包装。 + final data = await apiClient.get( + '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.from(json), + ); + }, + ); + return data; + } catch (error) { + debugPrint('getRecordList failed: $error'); + return null; + } } } 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 75c05a6..a36ca80 100644 --- a/lib/features/auth/view_model_auth/view_model_auth.dart +++ b/lib/features/auth/view_model_auth/view_model_auth.dart @@ -66,7 +66,8 @@ class AuthViewModel extends StateNotifier { 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; + if (data == null) return false; + if (data.items == null) 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 6296cc6..3efa2b6 100644 --- a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart +++ b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart @@ -11,26 +11,12 @@ import 'package:recording_tool/features/competition_teams/pages/page_competition import 'package:recording_tool/features/events/model/model_event_info.dart'; import 'package:recording_tool/features/events/pages/page_event_info.dart'; import 'package:recording_tool/features/events/view_model/view_model_event_info.dart'; -import 'package:recording_tool/features/recording/model/model_recording_context.dart'; import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart'; class ScanQrCodePage extends ConsumerStatefulWidget { const ScanQrCodePage({super.key}); - static const mockRtmpUrl = - 'rtmp://192.168.1.245:19090/蔡依婷vs夏志豪_空中格斗赛_高中组/蔡依婷vs夏志豪_空中格斗赛_高中组'; - - static const mockRecordingContext = RecordingContext( - eventTitle: '全国青少年无人机大赛', - matchName: '空中格斗赛', - group: '高中组', - venue: '场地 1', - time: '7月1日 12:00-15:00', - playerName: '蔡依婷vs夏志豪', - playerPhone: '', - ); - @override ConsumerState createState() => _ScanQrCodePageState(); } diff --git a/pubspec.yaml b/pubspec.yaml index eda8cbd..47952c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: apivideo_live_stream: path: plugins/apivideo_live_stream jwt_decoder: ^2.0.1 + network_info_plus: ^7.0.0 dev_dependencies: flutter_test: