优化扫码页 UI

This commit is contained in:
2026-07-28 18:50:14 +08:00
parent 8e599f5a86
commit f7f7413b55
8 changed files with 157 additions and 52 deletions
+3
View File
@@ -17,6 +17,9 @@ enum AuthApi {
/// 人工设置晋级/淘汰 /// 人工设置晋级/淘汰
setTeamStatus('/api/events/device/schedule/team/score'), setTeamStatus('/api/events/device/schedule/team/score'),
/// 获取登录设备信息
getLoginDeviceInfo('/api/events/device/nas/info'),
/// 获取选手的赛事信息 /// 获取选手的赛事信息
playerRegistrationList('/api/events/device/player/registration/list'); playerRegistrationList('/api/events/device/player/registration/list');
+17 -3
View File
@@ -38,11 +38,15 @@ class ApiClient {
return _parseData<T>(raw, parser); return _parseData<T>(raw, parser);
} }
if (raw is! Map<String, dynamic>) { final payload = raw is Map ? Map<String, dynamic>.from(raw) : null;
if (payload == null ||
!(payload.containsKey('code') || payload.containsKey('message'))) {
return _parseData<T>(raw, parser); return _parseData<T>(raw, parser);
} }
final wrapped = ApiResponse<T>.fromJson(raw, fromJsonT: parser); // 先按 {code,message,data} 解包,再用业务 parser 解析 data
// 避免把 Map 直接强转成业务模型。
final wrapped = ApiResponse<dynamic>.fromJson(payload);
if (!wrapped.isSuccess) { if (!wrapped.isSuccess) {
throw ApiException( throw ApiException(
code: wrapped.code, 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<T>(wrapped.data, parser);
} on DioException catch (error) { } on DioException catch (error) {
throw _mapDioException(error); throw _mapDioException(error);
} }
+17 -1
View File
@@ -11,10 +11,26 @@ class ApiResponse<T> {
Map<String, dynamic> json, { Map<String, dynamic> json, {
T Function(dynamic json)? fromJsonT, 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<T>( return ApiResponse<T>(
code: (json['code'] as num?)?.toInt() ?? 200, code: (json['code'] as num?)?.toInt() ?? 200,
message: (json['message'] ?? json['msg'] ?? '').toString(), message: (json['message'] ?? json['msg'] ?? '').toString(),
data: fromJsonT == null ? json['data'] as T? : fromJsonT(json['data']), data: data,
); );
} }
} }
+4
View File
@@ -18,6 +18,7 @@ class JwtDecodedData {
String? oId; String? oId;
List<String>? oIds; List<String>? oIds;
String? organizerId; String? organizerId;
String? nasIpAddr;
JwtDecodedData({ JwtDecodedData({
this.authType, this.authType,
@@ -28,6 +29,7 @@ class JwtDecodedData {
this.oId, this.oId,
this.oIds, this.oIds,
this.organizerId, this.organizerId,
this.nasIpAddr,
}); });
factory JwtDecodedData.fromJson(Map<String, dynamic> json) => JwtDecodedData( factory JwtDecodedData.fromJson(Map<String, dynamic> json) => JwtDecodedData(
@@ -41,6 +43,7 @@ class JwtDecodedData {
? [] ? []
: List<String>.from(json['oIds']!.map((x) => x?.toString())), : List<String>.from(json['oIds']!.map((x) => x?.toString())),
organizerId: json['organizerId']?.toString(), organizerId: json['organizerId']?.toString(),
nasIpAddr: json['nasIpAddr']?.toString(),
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@@ -52,6 +55,7 @@ class JwtDecodedData {
'oId': oId, 'oId': oId,
'oIds': oIds == null ? [] : List<dynamic>.from(oIds!.map((x) => x)), 'oIds': oIds == null ? [] : List<dynamic>.from(oIds!.map((x) => x)),
'organizerId': organizerId, 'organizerId': organizerId,
'nasIpAddr': nasIpAddr,
}; };
} }
+40 -21
View File
@@ -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/auth/view_model_auth/view_model_auth.dart';
import 'package:recording_tool/features/scan_qrcode/pages/page_scan_qrcode.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/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_dialog.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart';
@@ -23,14 +22,22 @@ class AuthPageWidget extends ConsumerStatefulWidget {
class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> { class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
late final TextEditingController _controller; late final TextEditingController _controller;
/// 记录点击次数
int _clickCount = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = TextEditingController(text: '905758'); _controller = TextEditingController(text: '');
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final token = AppStorage.getString(StorageKeys.authToken); final token = AppStorage.getString(StorageKeys.authToken);
if (token?.isNotEmpty ?? false) { if (token?.isNotEmpty ?? false) {
final success = await ref
.read(authProvider.notifier)
.parseTokenSetState();
if (!success) {
return;
}
AppNavigator.push(const ScanQrCodePage()); AppNavigator.push(const ScanQrCodePage());
} }
}); });
@@ -42,6 +49,12 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
super.dispose(); super.dispose();
} }
void _showDeviceCodeDialog() async {
final deviceCode = await DeviceUtils.deviceCode();
if (!mounted) return;
AppDialog.confirm(context, title: '设备码:$deviceCode');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final authState = ref.watch(authProvider); final authState = ref.watch(authProvider);
@@ -55,14 +68,11 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: Stack( body: Stack(
children: [ children: [
Positioned( Positioned.fill(
top: 0,
left: 0,
right: 0,
child: Image.asset( child: Image.asset(
_AuthAssets.pageBg, _AuthAssets.pageBg,
width: double.infinity, width: double.infinity,
fit: BoxFit.fitWidth, fit: BoxFit.fill,
), ),
), ),
SafeArea( SafeArea(
@@ -74,11 +84,20 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
SizedBox(height: 190.h), SizedBox(height: 190.h),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(24.r), borderRadius: BorderRadius.circular(24.r),
child: Image.asset( child: GestureDetector(
_AuthAssets.appIcon, onTap: () {
width: 82.w, _clickCount++;
height: 82.w, if (_clickCount >= 5) {
fit: BoxFit.cover, _showDeviceCodeDialog();
_clickCount = 0;
}
},
child: Image.asset(
_AuthAssets.appIcon,
width: 82.w,
height: 82.w,
fit: BoxFit.cover,
),
), ),
), ),
SizedBox(height: 18.h), SizedBox(height: 18.h),
@@ -95,15 +114,15 @@ class _AuthPageWidgetState extends ConsumerState<AuthPageWidget> {
isLoading: authState.isLoading, isLoading: authState.isLoading,
onPressed: _handleSubmit, onPressed: _handleSubmit,
), ),
SizedBox(height: 20.h), // SizedBox(height: 20.h),
AppButton( // AppButton(
onPressed: () async { // onPressed: () async {
final deviceCode = await DeviceUtils.deviceCode(); // final deviceCode = await DeviceUtils.deviceCode();
if (!mounted) return; // if (!mounted) return;
AppDialog.confirm(context, title: '设备码:$deviceCode'); // AppDialog.confirm(context, title: '设备码:$deviceCode');
}, // },
label: '获取设备码', // label: '获取设备码',
), // ),
], ],
), ),
), ),
+21
View File
@@ -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/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';
import 'package:recording_tool/features/auth/model/model_jwt.dart';
class AuthServer { class AuthServer {
/// [passCode] 口令码 /// [passCode] 口令码
@@ -55,4 +56,24 @@ class AuthServer {
return null; 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,12 +1,9 @@
import 'package:flutter/foundation.dart';
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/app_storage.dart';
import 'package:recording_tool/core/cache/storage_keys.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_auth.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/server/server_auth.dart';
import 'package:recording_tool/features/auth/state/state_auth.dart'; import 'package:recording_tool/features/auth/state/state_auth.dart';
@@ -33,7 +30,11 @@ class AuthViewModel extends StateNotifier<AuthState> {
await AppStorage.setString(StorageKeys.authToken, data.deviceAccessToken); await AppStorage.setString(StorageKeys.authToken, data.deviceAccessToken);
state = const AuthState(); state = const AuthState();
if (data.deviceAccessToken.isNotEmpty) { if (data.deviceAccessToken.isNotEmpty) {
parseTokenSetState(data.deviceAccessToken); final ok = await parseTokenSetState();
if (!ok) {
state = const AuthState(errorMessage: '获取赛事信息失败,请重试');
return false;
}
} }
return true; return true;
} on FormatException catch (error) { } on FormatException catch (error) {
@@ -49,18 +50,22 @@ class AuthViewModel extends StateNotifier<AuthState> {
} }
/// 解析 TOKEN,并更新状态 /// 解析 TOKEN,并更新状态
Future<bool> parseTokenSetState(String token) async { Future<bool> parseTokenSetState() async {
final decoded = JwtDecoder.decode(token); final data = await AuthServer.getLoginDeviceInfo(_ref);
final rawData = decoded['data']; if (data == null) return false;
if (rawData is! Map) return false; state = state.copyWith(jwtDecodedData: data);
try { return true;
final data = JwtDecodedData.fromJson(Map<String, dynamic>.from(rawData)); // final decoded = JwtDecoder.decode(token);
state = state.copyWith(jwtDecodedData: data); // final rawData = decoded['data'];
return true; // if (rawData is! Map) return false;
} catch (error) { // try {
debugPrint('认证失败,请重试: $error'); // final data = JwtDecodedData.fromJson(Map<String, dynamic>.from(rawData));
return false; // state = state.copyWith(jwtDecodedData: data);
} // return true;
// } catch (error) {
// debugPrint('认证失败,请重试: $error');
// return false;
// }
} }
/// 获取赛事列表 /// 获取赛事列表
@@ -218,18 +218,41 @@ class _BrandHeader extends StatelessWidget {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Image.asset( Expanded(
_ScanAssets.appIcon, child: Row(
width: 34.w, children: [
height: 34.w, ClipRRect(
fit: BoxFit.cover, 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), Row(
Image.asset( children: [
_ScanAssets.appNameText, Icon(Icons.chevron_left_rounded, color: Colors.black, size: 28.sp),
width: 117.w, Text(
height: 29.w, '返回',
fit: BoxFit.contain, style: TextStyle(
// color: const Color(0xFF53A7F3),
color: Colors.black,
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
],
), ),
], ],
); );