diff --git a/lib/app/config/api_common.dart b/lib/app/config/api_common.dart index 3b84cb3..fda1018 100644 --- a/lib/app/config/api_common.dart +++ b/lib/app/config/api_common.dart @@ -17,6 +17,9 @@ enum AuthApi { /// 人工设置晋级/淘汰 setTeamStatus('/api/events/device/schedule/team/score'), + /// 获取登录设备信息 + getLoginDeviceInfo('/api/events/device/nas/info'), + /// 获取选手的赛事信息 playerRegistrationList('/api/events/device/player/registration/list'); diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 3c6991e..fc3fb27 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -38,11 +38,15 @@ class ApiClient { return _parseData(raw, parser); } - if (raw is! Map) { + final payload = raw is Map ? Map.from(raw) : null; + if (payload == null || + !(payload.containsKey('code') || payload.containsKey('message'))) { return _parseData(raw, parser); } - final wrapped = ApiResponse.fromJson(raw, fromJsonT: parser); + // 先按 {code,message,data} 解包,再用业务 parser 解析 data, + // 避免把 Map 直接强转成业务模型。 + final wrapped = ApiResponse.fromJson(payload); if (!wrapped.isSuccess) { throw ApiException( code: wrapped.code, @@ -52,7 +56,17 @@ class ApiClient { ); } - return wrapped.data as T; + if (wrapped.data == null) { + if (null is T) return null as T; + throw ApiException( + code: wrapped.code, + statusCode: response.statusCode, + message: '响应 data 为空', + details: raw, + ); + } + + return _parseData(wrapped.data, parser); } on DioException catch (error) { throw _mapDioException(error); } diff --git a/lib/core/network/api_response.dart b/lib/core/network/api_response.dart index 1f9fb46..99aa70a 100644 --- a/lib/core/network/api_response.dart +++ b/lib/core/network/api_response.dart @@ -11,10 +11,26 @@ class ApiResponse { Map json, { T Function(dynamic json)? fromJsonT, }) { + final rawData = json['data']; + T? data; + if (rawData != null && fromJsonT != null) { + data = fromJsonT(rawData); + } else if (rawData != null && fromJsonT == null) { + // 无 parser 时仅在类型已匹配时接收,避免 Map 被强转为业务模型。 + if (rawData is T) { + data = rawData; + } else { + throw FormatException( + 'ApiResponse data 类型不匹配,且未提供 fromJsonT;' + '期望 $T,实际 ${rawData.runtimeType}', + ); + } + } + return ApiResponse( code: (json['code'] as num?)?.toInt() ?? 200, message: (json['message'] ?? json['msg'] ?? '').toString(), - data: fromJsonT == null ? json['data'] as T? : fromJsonT(json['data']), + data: data, ); } } diff --git a/lib/features/auth/model/model_jwt.dart b/lib/features/auth/model/model_jwt.dart index af3d24c..6974552 100644 --- a/lib/features/auth/model/model_jwt.dart +++ b/lib/features/auth/model/model_jwt.dart @@ -18,6 +18,7 @@ class JwtDecodedData { String? oId; List? 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 json) => JwtDecodedData( @@ -41,6 +43,7 @@ class JwtDecodedData { ? [] : List.from(json['oIds']!.map((x) => x?.toString())), organizerId: json['organizerId']?.toString(), + nasIpAddr: json['nasIpAddr']?.toString(), ); Map toJson() => { @@ -52,6 +55,7 @@ class JwtDecodedData { 'oId': oId, 'oIds': oIds == null ? [] : List.from(oIds!.map((x) => x)), 'organizerId': organizerId, + 'nasIpAddr': nasIpAddr, }; } diff --git a/lib/features/auth/pages/page_auth.dart b/lib/features/auth/pages/page_auth.dart index 42e8eba..393c0bc 100644 --- a/lib/features/auth/pages/page_auth.dart +++ b/lib/features/auth/pages/page_auth.dart @@ -9,7 +9,6 @@ import 'package:recording_tool/core/utils/device_utils.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'; @@ -23,14 +22,22 @@ class AuthPageWidget extends ConsumerStatefulWidget { class _AuthPageWidgetState extends ConsumerState { late final TextEditingController _controller; + /// 记录点击次数 + int _clickCount = 0; @override void initState() { super.initState(); - _controller = TextEditingController(text: '905758'); + _controller = TextEditingController(text: ''); WidgetsBinding.instance.addPostFrameCallback((_) async { 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()); } }); @@ -42,6 +49,12 @@ class _AuthPageWidgetState extends ConsumerState { 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); @@ -55,14 +68,11 @@ class _AuthPageWidgetState extends ConsumerState { 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( @@ -74,11 +84,20 @@ class _AuthPageWidgetState extends ConsumerState { 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), @@ -95,15 +114,15 @@ class _AuthPageWidgetState extends ConsumerState { 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: '获取设备码', + // ), ], ), ), diff --git a/lib/features/auth/server/server_auth.dart b/lib/features/auth/server/server_auth.dart index 2ccf884..415d18c 100644 --- a/lib/features/auth/server/server_auth.dart +++ b/lib/features/auth/server/server_auth.dart @@ -4,6 +4,7 @@ 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] 口令码 @@ -55,4 +56,24 @@ class AuthServer { return null; } } + + /// 获取登录设备信息 + static Future getLoginDeviceInfo(Ref ref) async { + try { + final apiClient = ref.read(apiClientProvider); + final data = await apiClient.get( + AuthApi.getLoginDeviceInfo.path, + parser: (json) { + if (json is! Map) { + throw const FormatException('登录设备信息响应格式错误'); + } + return JwtDecodedData.fromJson(Map.from(json)); + }, + ); + return data; + } catch (error) { + debugPrint('getLoginDeviceInfo 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 3beb76a..b223f6f 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,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_auth.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'; @@ -33,7 +30,11 @@ class AuthViewModel extends StateNotifier { 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) { @@ -49,18 +50,22 @@ class AuthViewModel extends StateNotifier { } /// 解析 TOKEN,并更新状态 - Future 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.from(rawData)); - state = state.copyWith(jwtDecodedData: data); - return true; - } catch (error) { - debugPrint('认证失败,请重试: $error'); - return false; - } + Future 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.from(rawData)); + // state = state.copyWith(jwtDecodedData: data); + // return true; + // } catch (error) { + // debugPrint('认证失败,请重试: $error'); + // return false; + // } } /// 获取赛事列表 diff --git a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart index 4f42ae0..76bad42 100644 --- a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart +++ b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart @@ -218,18 +218,41 @@ class _BrandHeader extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - Image.asset( - _ScanAssets.appIcon, - width: 34.w, - height: 34.w, - fit: BoxFit.cover, + Expanded( + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(17.r), + child: Image.asset( + _ScanAssets.appIcon, + width: 34.w, + height: 34.w, + fit: BoxFit.cover, + ), + ), + // SizedBox(width: .w), + Image.asset( + _ScanAssets.appNameText, + width: 117.w, + height: 29.w, + fit: BoxFit.contain, + ), + ], + ), ), - SizedBox(width: 8.w), - Image.asset( - _ScanAssets.appNameText, - width: 117.w, - height: 29.w, - fit: BoxFit.contain, + Row( + children: [ + Icon(Icons.chevron_left_rounded, color: Colors.black, size: 28.sp), + Text( + '返回', + style: TextStyle( + // color: const Color(0xFF53A7F3), + color: Colors.black, + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + ], ), ], );