diff --git a/assets/images/app_icon.png b/assets/images/image_app_icon.png similarity index 100% rename from assets/images/app_icon.png rename to assets/images/image_app_icon.png diff --git a/assets/images/image_app_name_text.png b/assets/images/image_app_name_text.png new file mode 100644 index 0000000..cf04e22 Binary files /dev/null and b/assets/images/image_app_name_text.png differ diff --git a/assets/images/image_copy.png b/assets/images/image_copy.png deleted file mode 100644 index fba2f9a..0000000 Binary files a/assets/images/image_copy.png and /dev/null differ diff --git a/assets/images/image_delete.png b/assets/images/image_delete.png deleted file mode 100644 index 7e2ee03..0000000 Binary files a/assets/images/image_delete.png and /dev/null differ diff --git a/assets/images/image_page_bg.png b/assets/images/image_page_bg.png new file mode 100644 index 0000000..f438ac4 Binary files /dev/null and b/assets/images/image_page_bg.png differ diff --git a/assets/images/image_scan_qrcode.png b/assets/images/image_scan_qrcode.png new file mode 100644 index 0000000..e839f9c Binary files /dev/null and b/assets/images/image_scan_qrcode.png differ diff --git a/lib/features/auth/model/model_jwt.dart b/lib/features/auth/model/model_jwt.dart index 1388741..adefb0d 100644 --- a/lib/features/auth/model/model_jwt.dart +++ b/lib/features/auth/model/model_jwt.dart @@ -15,9 +15,9 @@ class JwtDecodedData { int? deviceId; String? deviceRole; String? eventName; - double? oId; - List? oIds; - double? organizerId; + String? oId; + List? oIds; + String? organizerId; JwtDecodedData({ this.authType, @@ -36,11 +36,11 @@ class JwtDecodedData { deviceId: json["deviceId"], deviceRole: json["deviceRole"], eventName: json["eventName"], - oId: json["oId"]?.toDouble(), + oId: json["oId"]?.toString(), oIds: json["oIds"] == null ? [] - : List.from(json["oIds"]!.map((x) => x?.toDouble())), - organizerId: json["organizerId"]?.toDouble(), + : List.from(json["oIds"]!.map((x) => x?.toString())), + organizerId: json["organizerId"]?.toString(), ); Map toJson() => { diff --git a/lib/features/auth/pages/page_auth.dart b/lib/features/auth/pages/page_auth.dart index 5aa648e..7e38233 100644 --- a/lib/features/auth/pages/page_auth.dart +++ b/lib/features/auth/pages/page_auth.dart @@ -8,7 +8,10 @@ import 'package:recording_tool/core/cache/storage_keys.dart'; 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/shared/widgets/widgets.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'; class AuthPageWidget extends ConsumerStatefulWidget { const AuthPageWidget({super.key}); @@ -18,13 +21,12 @@ class AuthPageWidget extends ConsumerStatefulWidget { } class _AuthPageWidgetState extends ConsumerState { - late TextEditingController? _controller; + late final TextEditingController _controller; @override void initState() { super.initState(); - _controller = TextEditingController(); - _controller?.text = '986570'; + _controller = TextEditingController(text: '999779'); WidgetsBinding.instance.addPostFrameCallback((_) async { final token = AppStorage.getString(StorageKeys.authToken); @@ -36,7 +38,7 @@ class _AuthPageWidgetState extends ConsumerState { @override void dispose() { - _controller?.dispose(); + _controller.dispose(); super.dispose(); } @@ -44,88 +46,204 @@ class _AuthPageWidgetState extends ConsumerState { Widget build(BuildContext context) { final authState = ref.watch(authProvider); - return Center( - child: Column( - children: [ - SizedBox(height: 180.h), - AppText('裁判工作台', fontSize: 30.sp), - SizedBox(height: 20.h), - Text( - '输入执裁口令', - style: TextStyle(fontSize: 18.sp, color: Colors.black), - ), - SizedBox(height: 20.h), - - Container( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Column( - children: [ - AppTextField( - controller: _controller, - keyboardType: TextInputType.number, - maxLength: 6, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(6), - ], - ), - SizedBox(height: 20.h), - SizedBox( - width: double.maxFinite, - child: AppButton( - label: '确定', - onPressed: () async { - final code = _controller?.text; - final success = await ref - .read(authProvider.notifier) - .auth(code ?? ''); - if (!mounted) return; - if (success) { - AppNavigator.push(const ScanQrCodePage()); - return; - } - final message = ref.read(authProvider).errorMessage; - if (message != null && message.isNotEmpty) { - AppToast.show(message); - } - }, - variant: AppButtonVariant.secondary, - isLoading: authState.isLoading, + return AnnotatedRegion( + value: SystemUiOverlayStyle.dark.copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: Colors.white, + ), + child: Scaffold( + backgroundColor: Colors.white, + body: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: Image.asset( + _AuthAssets.pageBg, + width: double.infinity, + fit: BoxFit.fitWidth, + ), + ), + SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 32.w), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 190.h), + ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Image.asset( + _AuthAssets.appIcon, + width: 82.w, + height: 82.w, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 18.h), + Image.asset( + _AuthAssets.appNameText, + width: 132.w, + height: 28.w, + fit: BoxFit.cover, + ), + SizedBox(height: 78.h), + _PassCodeInput(controller: _controller), + SizedBox(height: 20.h), + _GradientConfirmButton( + 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: '获取设备码', + ), + ], ), ), - ], - ), - ), - ..._buildTestArea(authState.isLoading), - ], - ), - ); - } - - /// 测试区域 - List _buildTestArea(bool isLoading) { - return [ - SizedBox(height: 20.h), - Container( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Column( - children: [ - SizedBox( - width: double.maxFinite, - child: AppButton( - label: '获取设备码', - onPressed: () async { - DeviceUtils.deviceCode().then((code) { - AppDialog.confirm(context, title: '设备码', message: code); - }); - }, - variant: AppButtonVariant.secondary, - isLoading: isLoading, ), ), ], ), ), - ]; + ); + } + + Future _handleSubmit() async { + final success = await ref + .read(authProvider.notifier) + .auth(_controller.text); + if (!mounted) return; + if (success) { + AppNavigator.push(const ScanQrCodePage()); + return; + } + final message = ref.read(authProvider).errorMessage; + if (message != null && message.isNotEmpty) { + AppToast.show(message); + } } } + +class _PassCodeInput extends StatelessWidget { + const _PassCodeInput({required this.controller}); + + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + return Container( + height: 50.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 10.r, + offset: Offset(0, 2.h), + ), + ], + ), + child: TextField( + controller: controller, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + maxLength: 6, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(6), + ], + style: TextStyle( + color: const Color(0xFF2F3338), + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + decoration: InputDecoration( + hintText: '请输入执裁口令', + hintStyle: TextStyle( + color: const Color(0xFFB6B9BF), + fontSize: 16.sp, + fontWeight: FontWeight.w400, + ), + counterText: '', + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 14.h, + ), + ), + ), + ); + } +} + +class _GradientConfirmButton extends StatelessWidget { + const _GradientConfirmButton({ + required this.isLoading, + required this.onPressed, + }); + + final bool isLoading; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: isLoading ? 0.72 : 1, + child: GestureDetector( + onTap: isLoading ? null : onPressed, + child: Container( + height: 50.h, + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.r), + gradient: const LinearGradient( + colors: [Color(0xFF268DFF), Color(0xFF5DD4F6)], + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF2196F3).withValues(alpha: 0.28), + blurRadius: 14.r, + offset: Offset(0, 6.h), + ), + ], + ), + child: isLoading + ? SizedBox.square( + dimension: 18.r, + child: CircularProgressIndicator( + strokeWidth: 2.r, + valueColor: const AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : Text( + '确定', + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } +} + +class _AuthAssets { + const _AuthAssets._(); + + static String pageBg = Assets.images.imagePageBg.path; + static String appIcon = Assets.images.imageAppIcon.path; + static String appNameText = Assets.images.imageAppNameText.path; +} 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 49ddb13..a11077a 100644 --- a/lib/features/auth/view_model_auth/view_model_auth.dart +++ b/lib/features/auth/view_model_auth/view_model_auth.dart @@ -50,8 +50,13 @@ class AuthViewModel extends StateNotifier { Future parseTokenSetState(String token) async { final decoded = JwtDecoder.decode(token); if (decoded['data'] != null && decoded['data'] is Map) { - final data = JwtDecodedData.fromJson(decoded['data']); - state = state.copyWith(jwtDecodedData: data); + try { + final data = JwtDecodedData.fromJson(decoded['data']); + state = state.copyWith(jwtDecodedData: data); + } catch (error) { + state = const AuthState(errorMessage: '认证失败,请重试'); + return false; + } return true; } 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 21b0c5e..8a5e1c9 100644 --- a/lib/features/scan_qrcode/pages/page_scan_qrcode.dart +++ b/lib/features/scan_qrcode/pages/page_scan_qrcode.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -12,8 +13,6 @@ 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_bar.dart'; -import 'package:recording_tool/shared/widgets/app_button.dart'; import 'package:recording_tool/shared/widgets/app_qr_scanner_dialog.dart'; import 'package:recording_tool/shared/widgets/app_toast.dart'; @@ -34,10 +33,10 @@ class ScanQrCodePage extends ConsumerStatefulWidget { ); @override - ConsumerState createState() => _AuthPageWidgetState(); + ConsumerState createState() => _ScanQrCodePageState(); } -class _AuthPageWidgetState extends ConsumerState { +class _ScanQrCodePageState extends ConsumerState { @override void initState() { super.initState(); @@ -58,109 +57,106 @@ class _AuthPageWidgetState extends ConsumerState { @override Widget build(BuildContext context) { + final eventName = ref.watch( + authProvider.select( + (state) => state.jwtDecodedData?.eventName?.trim() ?? '', + ), + ); + return PopScope( canPop: true, onPopInvokedWithResult: (didPop, result) async { if (!didPop) return; await ref.read(authProvider.notifier).clearAuth(); }, - child: Scaffold( - appBar: myAppBar(context: context, title: '扫码'), - body: Center( - child: Column( + child: AnnotatedRegion( + value: SystemUiOverlayStyle.dark.copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: Colors.white, + ), + child: Scaffold( + backgroundColor: Colors.white, + body: Stack( children: [ - SizedBox(height: 100.h), - Text( - '裁判工作台', - style: TextStyle(fontSize: 30, color: Colors.black), + Positioned( + top: 0, + left: 0, + right: 0, + child: Image.asset( + _ScanAssets.pageBg, + width: double.infinity, + fit: BoxFit.fitWidth, + ), ), - SizedBox(height: 100.h), - Text( - '扫描选手参赛凭证进行执裁', - style: TextStyle(fontSize: 30, color: Colors.black), - ), - SizedBox(height: 20.h), - - // Consumer( - // builder: (context, ref, child) { - // return SizedBox( - // width: 280.w, - // height: 80.h, - // child: AppButton( - // label: '查看录像', - // onPressed: () async { - // final data = ref.watch( - // authProvider.select((state) => state.jwtDecodedData), - // ); - // if (data == null) return; - // debugPrint('赛事名字: ${data.eventName}'); - // final eventName = data.eventName ?? ''; - // await ref - // .read(authProvider.notifier) - // .getRecordList(eventName); - // }, - // variant: AppButtonVariant.secondary, - // ), - // ); - // }, - // ), - SizedBox(height: 16.h), - - Consumer( - builder: (context, ref, child) { - return SizedBox( - width: 280.w, - height: 80.h, - child: AppButton( - label: '参赛队伍', - onPressed: () async { - await AppNavigator.push( - const CompetitionTeamListPage(), - context: context, - ); - }, - variant: AppButtonVariant.secondary, - ), - ); - }, - ), - SizedBox(height: 16.h), - SizedBox( - width: 280.w, - height: 80.h, - child: AppButton( - label: '扫码', - onPressed: () async { - final String? playerId = await AppQrScannerDialog.show( - context, - ); - if (playerId == null || playerId.isEmpty) { - AppToast.show('查询选手信息失败'); - return; - } - EasyLoading.show(status: '查询选手信息...'); - try { - final success = await ref - .read(eventInfoProvider.notifier) - .loadRegistrationList( - request: PlayerRegistrationListReq( - userId: playerId, + SafeArea( + child: Padding( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 32.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _BrandHeader(), + SizedBox(height: 26.h), + Text( + eventName.isEmpty ? '赛事信息' : eventName, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF3C3F44), + fontSize: 24.sp, + fontWeight: FontWeight.w700, + height: 1.25, + ), + ), + const Spacer(flex: 134), + Center( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleScan, + child: Image.asset( + _ScanAssets.scanQrcode, + width: 166.w, + height: 165.w, + fit: BoxFit.contain, + ), + ), + ), + SizedBox(height: 28.h), + Center( + child: Text( + '扫描选手参赛凭证进行执裁', + style: TextStyle( + color: const Color(0xFFABAFB6), + fontSize: 16.sp, + fontWeight: FontWeight.w400, + height: 1.2, + ), + ), + ), + const Spacer(flex: 211), + Row( + children: [ + Expanded( + child: _BottomOutlineButton( + label: '查看录像', + onPressed: _handleViewRecords, ), - ); - EasyLoading.dismiss(); - if (!success) { - AppToast.show('查询选手信息失败'); - return; - } - if (!mounted) return; - - AppNavigator.push(EventInfoPage(playerId: playerId)); - } catch (error) { - AppToast.show('查询选手信息失败'); - EasyLoading.dismiss(); - } - }, - variant: AppButtonVariant.secondary, + ), + SizedBox(width: 12.w), + Expanded( + child: _BottomOutlineButton( + label: '参赛队伍', + onPressed: () async { + await AppNavigator.push( + const CompetitionTeamListPage(), + context: context, + ); + }, + ), + ), + ], + ), + ], + ), ), ), ], @@ -169,4 +165,125 @@ class _AuthPageWidgetState extends ConsumerState { ), ); } + + Future _handleScan() async { + final String? playerId = await AppQrScannerDialog.show(context); + if (playerId == null || playerId.isEmpty) { + AppToast.show('查询选手信息失败'); + return; + } + EasyLoading.show(status: '查询选手信息...'); + try { + final success = await ref + .read(eventInfoProvider.notifier) + .loadRegistrationList( + request: PlayerRegistrationListReq(userId: playerId), + ); + EasyLoading.dismiss(); + if (!success) { + AppToast.show('查询选手信息失败'); + return; + } + if (!mounted) return; + + AppNavigator.push(EventInfoPage(playerId: playerId)); + } catch (error) { + EasyLoading.dismiss(); + AppToast.show('查询选手信息失败'); + } + } + + Future _handleViewRecords() async { + final eventName = ref.read(authProvider).jwtDecodedData?.eventName ?? ''; + if (eventName.trim().isEmpty) { + AppToast.show('暂无赛事信息'); + return; + } + + EasyLoading.show(status: '查询录像...'); + try { + final success = await ref + .read(authProvider.notifier) + .getRecordList(eventName); + EasyLoading.dismiss(); + if (!success) { + AppToast.show('暂无录像'); + return; + } + AppToast.show('录像列表已更新'); + } catch (error) { + EasyLoading.dismiss(); + AppToast.show('查询录像失败'); + } + } +} + +class _BrandHeader extends StatelessWidget { + const _BrandHeader(); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + _ScanAssets.appIcon, + width: 34.w, + height: 34.w, + fit: BoxFit.cover, + ), + SizedBox(width: 8.w), + Image.asset( + _ScanAssets.appNameText, + width: 117.w, + height: 29.w, + fit: BoxFit.contain, + ), + ], + ); + } +} + +class _BottomOutlineButton extends StatelessWidget { + const _BottomOutlineButton({required this.label, required this.onPressed}); + + final String label; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 38.h, + child: Material( + color: Colors.white.withValues(alpha: 0.08), + shape: StadiumBorder( + side: BorderSide(color: const Color(0xFF9CCEFF), width: 1.r), + ), + child: InkWell( + onTap: onPressed, + customBorder: const StadiumBorder(), + child: Center( + child: Text( + label, + style: TextStyle( + color: const Color(0xFF53A7F3), + fontSize: 14.sp, + fontWeight: FontWeight.w500, + height: 1.2, + ), + ), + ), + ), + ), + ); + } +} + +class _ScanAssets { + const _ScanAssets._(); + + static const pageBg = 'assets/images/image_page_bg.png'; + static const appIcon = 'assets/images/image_app_icon.png'; + static const appNameText = 'assets/images/image_app_name_text.png'; + static const scanQrcode = 'assets/images/image_scan_qrcode.png'; } diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 80a04cf..176be07 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -24,13 +24,13 @@ class $AssetsHtmlGen { class $AssetsImagesGen { const $AssetsImagesGen(); - /// File path: assets/images/image_copy.png - AssetGenImage get imageCopy => - const AssetGenImage('assets/images/image_copy.png'); + /// File path: assets/images/image_app_icon.png + AssetGenImage get imageAppIcon => + const AssetGenImage('assets/images/image_app_icon.png'); - /// File path: assets/images/image_delete.png - AssetGenImage get imageDelete => - const AssetGenImage('assets/images/image_delete.png'); + /// File path: assets/images/image_app_name_text.png + AssetGenImage get imageAppNameText => + const AssetGenImage('assets/images/image_app_name_text.png'); /// File path: assets/images/image_dialog_bg.png AssetGenImage get imageDialogBg => @@ -40,16 +40,26 @@ class $AssetsImagesGen { AssetGenImage get imageLogo => const AssetGenImage('assets/images/image_logo.png'); + /// File path: assets/images/image_page_bg.png + AssetGenImage get imagePageBg => + const AssetGenImage('assets/images/image_page_bg.png'); + + /// File path: assets/images/image_scan_qrcode.png + AssetGenImage get imageScanQrcode => + const AssetGenImage('assets/images/image_scan_qrcode.png'); + /// File path: assets/images/image_vs.png AssetGenImage get imageVs => const AssetGenImage('assets/images/image_vs.png'); /// List of all assets List get values => [ - imageCopy, - imageDelete, + imageAppIcon, + imageAppNameText, imageDialogBg, imageLogo, + imagePageBg, + imageScanQrcode, imageVs, ]; }