优化扫码页 UI
This commit is contained in:
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: '获取设备码',
|
||||||
),
|
// ),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user