diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f45e8a7d..69a1d9d2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,6 @@ - - @@ -11,24 +9,19 @@ + + + - + - - - - - - - - - - - - - - - + diff --git a/dist/version/check.json b/dist/version/check.json new file mode 100644 index 00000000..bbd4610b --- /dev/null +++ b/dist/version/check.json @@ -0,0 +1,9 @@ +{ + "hasUpdate": true, + "patch": { + "version": "1.0.2", + "patchUrl": "http://127.0.0.1:8080/libapp.so", + "md5": "", + "targetVersionCode": 1 + } +} diff --git a/lib/core/consts/http_api_consts.dart b/lib/core/consts/http_api_consts.dart index 819bab41..4a50d415 100644 --- a/lib/core/consts/http_api_consts.dart +++ b/lib/core/consts/http_api_consts.dart @@ -16,6 +16,15 @@ class HttpApiConsts { // 切换设备 static const String switchDevice = "$baseUrl/forward/device/switchDevice"; + // 获取光伏电站列表 + static const String getSiteList = "$baseUrl/system/site/list"; + + // 获取场站下的无人机机场列表 + static const String getSiteUAVList = "$baseUrl/iot/UAV/getSiteUAVList"; + + // 获取UAV状态详情 + static const String getUAVState = "$baseUrl/iot/UAV/getUAVState"; + // 获取设备位置 static const String getDeviceLocation = "$baseUrl/iot/device/userDevice"; diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index 0b833be9..78062191 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -57,15 +57,27 @@ import '../../features/remote_control/data/datasources/remote_tcp_datasource.dar import '../../features/remote_control/domain/usecase/remote_control_usecase.dart'; import '../../features/remote_control/presentation/bloc/remote_control_cubit.dart'; import '../../features/v2/home/data/datasources/home_remote_datasource.dart'; +import '../../features/v2/home/data/datasources/site_datasource.dart'; +import '../../features/v2/home/data/datasources/site_datasource_impl.dart'; import '../../features/v2/home/data/repositories/home_repository_impl.dart'; +import '../../features/v2/home/data/repositories/site_repository_impl.dart'; import '../../features/v2/home/domain/repositories/home_repository.dart'; +import '../../features/v2/home/domain/repositories/site_repository.dart'; import '../../features/v2/home/domain/usecases/get_home_data_usecase.dart'; +import '../../features/v2/home/domain/usecases/get_site_list_usecase.dart'; import '../../features/v2/home/presentation/bloc/home_v2_bloc.dart'; +import '../../features/v2/site/presentation/cubit/site_cubit.dart'; import '../../features/v2/device_list/data/datasources/device_remote_datasource.dart' as device_v2; import '../../features/v2/device_list/data/repositories/device_repository_impl.dart' as device_v2_repo; import '../../features/v2/device_list/domain/repositories/device_repository.dart' as device_v2_domain; import '../../features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart' as device_v2_usecase; import '../../features/v2/device_list/presentation/bloc/device_status_bloc.dart' as device_v2_bloc; +import '../../features/v2/device_list/data/datasources/drone_station_datasource.dart'; +import '../../features/v2/device_list/data/datasources/drone_station_datasource_impl.dart'; +import '../../features/v2/device_list/data/repositories/drone_station_repository_impl.dart'; +import '../../features/v2/device_list/domain/repositories/drone_station_repository.dart'; +import '../../features/v2/device_list/domain/usecases/get_drone_station_list_usecase.dart'; +import '../../features/v2/device_list/presentation/bloc/drone_station_bloc.dart'; import '../../features/v2/waring_center/data/datasources/alarm_remote_datasource.dart'; import '../../features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart'; import '../../features/v2/waring_center/data/repositories/alarm_repository_impl.dart'; @@ -204,7 +216,13 @@ Future init() async { sl.registerLazySingleton(() => HomeRemoteDataSourceImpl()); sl.registerLazySingleton(() => HomeRepositoryImpl(sl())); sl.registerLazySingleton(() => GetHomeDataUseCase(sl())); - sl.registerFactory(() => HomeV2Bloc(sl())); + + // Site (场站) + sl.registerLazySingleton(() => SiteDataSourceImpl(sl())); + sl.registerLazySingleton(() => SiteRepositoryImpl(sl())); + sl.registerLazySingleton(() => GetSiteListUseCase(sl())); + + sl.registerFactory(() => HomeV2Bloc(sl(), sl(), sl(), sl())); /// Device Status V2 sl.registerLazySingleton(() => device_v2.DeviceRemoteDataSourceImpl()); @@ -212,6 +230,13 @@ Future init() async { sl.registerLazySingleton(() => device_v2_usecase.GetDeviceStatusDataUseCase(repository: sl())); sl.registerFactory(() => device_v2_bloc.DeviceStatusBloc(sl())); + /// Drone Station V2 + sl.registerLazySingleton(() => DroneStationDataSourceImpl(sl())); + sl.registerLazySingleton(() => DroneStationRepositoryImpl(sl())); + sl.registerLazySingleton(() => GetDroneStationListUseCase(sl())); + sl.registerLazySingleton(() => GetUAVDetailUseCase(sl())); + sl.registerFactory(() => DroneStationBloc(sl(), sl())); + /// Alarm Center V2 sl.registerLazySingleton(() => AlarmRemoteDataSourceImpl()); sl.registerLazySingleton(() => AlarmRepositoryImpl(sl())); @@ -226,6 +251,9 @@ Future init() async { /// 5. 状态管理 (Cubit/Bloc) sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册 + // 🔥 SiteCubit (全局共享,持久化选中场站) + sl.registerLazySingleton(() => SiteCubit(sl())); + // 🔥 语言管理 Cubit (单例) sl.registerLazySingleton(() => LocaleCubit()); diff --git a/lib/core/domain/entities/user_entity.dart b/lib/core/domain/entities/user_entity.dart index 62fcc013..61c4f31f 100644 --- a/lib/core/domain/entities/user_entity.dart +++ b/lib/core/domain/entities/user_entity.dart @@ -5,6 +5,7 @@ class UserEntity extends Equatable { final String username; final String nickname; final String token; + final int orgId; // 组织ID,用于获取场站列表 final String? avatar; final String? email; final String? phone; @@ -14,6 +15,7 @@ class UserEntity extends Equatable { required this.username, required this.nickname, required this.token, + required this.orgId, this.avatar, this.email, this.phone, @@ -26,6 +28,7 @@ class UserEntity extends Equatable { nickname, avatar, token, + orgId, email, phone, ]; diff --git a/lib/core/update/update_cubit.dart b/lib/core/update/update_cubit.dart index 39aee886..fc18ab01 100644 --- a/lib/core/update/update_cubit.dart +++ b/lib/core/update/update_cubit.dart @@ -1,7 +1,10 @@ +import 'dart:io'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/services.dart'; import 'package:logger/logger.dart'; import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'version_check_service.dart'; import 'update_state.dart'; @@ -81,7 +84,7 @@ class UpdateCubit extends Cubit { } } - /// 下载并安装完整 APK + /// 自动下载并安装 APK(App 内下载) Future downloadAndInstallApk(String apkUrl) async { emit(const UpdateDownloading(0.0, isPatch: false)); @@ -93,16 +96,19 @@ class UpdateCubit extends Cubit { }); if (apkPath != null) { - emit(const UpdateInstalling(isPatch: false)); - - // 🔥 整包更新后,清除补丁版本记录 + _logger.i('✅ APK 下载完成: $apkPath'); + // 🔥 关键:先清理版本号记录,并等待用户看到提示 + emit(const UpdateClearingVersion()); + _logger.i('🗑️ 开始清除补丁版本记录'); await _versionService.clearPatchVersionInfo(); + _logger.i('✅ 补丁版本记录已清除'); - // 打开 APK 文件,触发系统安装界面 - final result = await OpenFile.open(apkPath); - _logger.i('📦 安装结果: ${result.message}'); - - emit(UpdateSuccess()); + // 🔥 短暂延迟,让用户看到清理完成的提示 + await Future.delayed(const Duration(milliseconds: 800)); + + // 🔥 直接调起系统安装页面,不再等待用户点击 + _logger.i('📦 自动调起系统安装页面'); + await triggerInstall(apkPath); } else { emit(const UpdateFailure('APK 下载失败')); } @@ -112,6 +118,97 @@ class UpdateCubit extends Cubit { } } + /// 触发系统安装页面 + Future triggerInstall(String apkPath) async { + emit(const UpdateInstalling(isPatch: false)); + + try { + _logger.i('📦 开始调起系统安装页面'); + _logger.i('📁 APK 路径: $apkPath'); + + // 🔥 验证文件是否存在 + final file = File(apkPath); + if (!await file.exists()) { + _logger.e('❌ APK 文件不存在: $apkPath'); + emit(UpdateFailure('APK 文件不存在,请重新下载')); + return; + } + + final fileSize = await file.length(); + _logger.i('📏 APK 文件大小: ${fileSize / 1024 / 1024} MB'); + + // 🔥 关键修复:将APK复制到外部存储,避免某些Android版本的安装限制 + final externalDir = await getExternalStorageDirectory(); + String finalApkPath = apkPath; + + if (externalDir != null) { + final publicApkPath = '${externalDir.path}/app-update-final.apk'; + _logger.i('📋 复制APK到外部存储: $publicApkPath'); + + try { + await file.copy(publicApkPath); + finalApkPath = publicApkPath; + _logger.i('✅ APK复制成功'); + } catch (e) { + _logger.w('⚠️ 复制APK失败,使用原路径: $e'); + } + } + + // 打开 APK 文件,触发系统安装界面 + _logger.i('🚀 调用 OpenFile.open...'); + _logger.i('🚀 最终APK路径: $finalApkPath'); + final result = await OpenFile.open(finalApkPath); + _logger.i('📦 安装结果: ${result.message} (type: ${result.type})'); + + // 如果用户取消安装,显示提示信息并保留文件路径 + if (result.type != ResultType.done) { + _logger.w('⚠️ 用户取消了安装或安装失败 (type: ${result.type})'); + + // 🔥 如果是权限问题,提供更友好的提示 + String errorMsg = '您取消了安装'; + if (result.type == ResultType.noAppToOpen) { + errorMsg = '没有找到可以安装 APK 的应用,请检查系统设置'; + } else if (result.type == ResultType.permissionDenied) { + errorMsg = '没有安装权限,请在系统设置中允许"安装未知应用"'; + } else if (result.type == ResultType.error) { + errorMsg = '安装出错: ${result.message}'; + } + + emit(UpdateFailure('$errorMsg,APK 文件位于: $finalApkPath')); + } else { + // 安装成功,关闭对话框 + emit(UpdateSuccess()); + } + } catch (e) { + _logger.e('❌ 调起安装页面异常: $e'); + emit(UpdateFailure('调起安装页面失败: $e')); + } + } + + /// 在浏览器中打开下载链接(手动下载) + Future openInBrowser(String apkUrl) async { + try { + _logger.i('🌐 在浏览器中打开 APK 下载链接'); + + final uri = Uri.parse(apkUrl); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + _logger.i('✅ 已打开浏览器下载'); + + // 🔥 整包更新后,清除补丁版本记录 + await _versionService.clearPatchVersionInfo(); + + emit(UpdateSuccess()); + } else { + _logger.e('❌ 无法打开浏览器'); + emit(const UpdateFailure('无法打开浏览器')); + } + } catch (e) { + _logger.e('❌ 打开浏览器异常: $e'); + emit(UpdateFailure('打开浏览器失败: $e')); + } + } + /// 取消更新 void cancelUpdate() { _logger.i('❌ 用户取消更新'); diff --git a/lib/core/update/update_dialog.dart b/lib/core/update/update_dialog.dart index cc12771a..13d1943f 100644 --- a/lib/core/update/update_dialog.dart +++ b/lib/core/update/update_dialog.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'update_cubit.dart'; import 'update_state.dart'; import 'version_check_service.dart'; -/// 更新对话框 +/// 更新对话框(带完整流程提示) class UpdateDialog extends StatelessWidget { final AppVersionInfo versionInfo; @@ -15,19 +16,28 @@ class UpdateDialog extends StatelessWidget { return BlocListener( listener: (context, state) { if (state is UpdateSuccess) { - // 更新成功,关闭对话框 Navigator.of(context).pop(); if (versionInfo.updateType == 'patch') { - // 差量更新需要重启 _showRestartDialog(context); } - // 整包更新会自动打开安装界面,不需要额外操作 } else if (state is UpdateFailure) { Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('更新失败: ${state.error}')), - ); + + String errorMessage = state.error; + bool showApkPath = false; + String? apkPath; + + if (errorMessage.contains('APK 文件位于:')) { + final parts = errorMessage.split('APK 文件位于:'); + if (parts.length > 1) { + apkPath = parts[1].trim(); + showApkPath = true; + errorMessage = '您取消了安装'; + } + } + + _showErrorDialog(context, errorMessage, showApkPath: showApkPath, apkPath: apkPath); } }, child: AlertDialog( @@ -72,27 +82,64 @@ class UpdateDialog extends StatelessWidget { BlocBuilder( builder: (context, state) { if (state is UpdateDownloading) { - return SizedBox( - width: 60, - height: 60, - child: Stack( - alignment: Alignment.center, - children: [ - CircularProgressIndicator( - value: state.progress, - strokeWidth: 3, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + state.isPatch ? '⬇️ 正在下载补丁...' : '⬇️ 正在下载 APK...', + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + SizedBox( + width: 60, + height: 60, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: state.progress, + strokeWidth: 3, + ), + Text('${(state.progress * 100).toInt()}%'), + ], ), - Text('${(state.progress * 100).toInt()}%'), - ], - ), + ), + ], + ); + } else if (state is UpdateClearingVersion) { + return const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('✅ 版本号清理完成', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)), + SizedBox(height: 4), + Text('正在调起安装页面...', style: TextStyle(fontSize: 12)), + SizedBox(height: 8), + CircularProgressIndicator(), + ], + ); + } else if (state is UpdateReadyToInstall) { + return ElevatedButton.icon( + onPressed: () { + context.read().triggerInstall(state.apkPath); + }, + icon: const Icon(Icons.install_mobile), + label: const Text('去安装'), ); } else if (state is UpdateInstalling) { - return const Text('安装中...'); + return const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('📦 正在打开安装页面...', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)), + SizedBox(height: 4), + Text('请在系统安装界面确认安装', style: TextStyle(fontSize: 12)), + SizedBox(height: 8), + CircularProgressIndicator(), + ], + ); } else { return ElevatedButton( onPressed: () { if (versionInfo.updateType == 'patch' && versionInfo.patchUrl != null) { - // 差量更新 context.read().applyPatch( versionInfo.patchUrl!, versionInfo.version, @@ -100,7 +147,6 @@ class UpdateDialog extends StatelessWidget { md5: versionInfo.patchMd5, ); } else if (versionInfo.apkUrl != null) { - // 整包更新 context.read().downloadAndInstallApk(versionInfo.apkUrl!); } }, @@ -129,8 +175,7 @@ class UpdateDialog extends StatelessWidget { ElevatedButton( onPressed: () { Navigator.of(ctx).pop(); - // TODO: 实现重启逻辑 - // 可以使用 restart_app 包或退出应用让用户手动打开 + SystemNavigator.pop(); }, child: const Text('立即重启'), ), @@ -138,4 +183,138 @@ class UpdateDialog extends StatelessWidget { ), ); } + + void _showErrorDialog(BuildContext context, String message, {bool showApkPath = false, String? apkPath}) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('更新提示'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message), + if (showApkPath && apkPath != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '📁 APK 文件位置', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13), + ), + const SizedBox(height: 6), + Text( + apkPath, + style: const TextStyle(fontSize: 11, fontFamily: 'monospace'), + ), + const SizedBox(height: 6), + const Text( + '您可以到该目录手动点击 APK 文件进行安装', + style: TextStyle(fontSize: 11, color: Colors.grey), + ), + ], + ), + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('确定'), + ), + ], + ), + ); + } +} + +/// 显示整包更新提示(在浏览器中下载) +class FullApkUpdateDialog extends StatelessWidget { + final AppVersionInfo versionInfo; + + const FullApkUpdateDialog({super.key, required this.versionInfo}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('发现新版本'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('版本号: ${versionInfo.version}'), + const SizedBox(height: 8), + const Text('更新类型: 整包更新'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '📱 整包更新说明', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + SizedBox(height: 8), + Text( + '• 点击"立即下载"将在浏览器中打开下载链接\n' + '• 下载完成后请手动安装 APK\n' + '• 安装完成后重启应用即可', + style: TextStyle(fontSize: 13, height: 1.5), + ), + ], + ), + ), + const SizedBox(height: 12), + if (versionInfo.updateDesc.isNotEmpty) ...[ + const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(versionInfo.updateDesc), + ], + ], + ), + ), + actions: [ + if (!versionInfo.forceUpdate) + TextButton( + onPressed: () { + context.read().cancelUpdate(); + Navigator.of(context).pop(); + }, + child: const Text('稍后'), + ), + ElevatedButton( + onPressed: () { + if (versionInfo.apkUrl != null) { + context.read().downloadAndInstallApk(versionInfo.apkUrl!); + Navigator.of(context).pop(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('正在浏览器中打开下载链接...'), + duration: Duration(seconds: 2), + ), + ); + } + }, + child: const Text('立即下载'), + ), + ], + ); + } } diff --git a/lib/core/update/update_state.dart b/lib/core/update/update_state.dart index 72ffaa2c..b9442670 100644 --- a/lib/core/update/update_state.dart +++ b/lib/core/update/update_state.dart @@ -30,6 +30,18 @@ class UpdateDownloading extends UpdateState { const UpdateDownloading(this.progress, {required this.isPatch}); } +/// 清理版本号中 +class UpdateClearingVersion extends UpdateState { + const UpdateClearingVersion(); +} + +/// 准备安装 +class UpdateReadyToInstall extends UpdateState { + final String apkPath; + + const UpdateReadyToInstall(this.apkPath); +} + /// 安装中 class UpdateInstalling extends UpdateState { final bool isPatch; diff --git a/lib/core/update/version_check_service.dart b/lib/core/update/version_check_service.dart index a5c724ce..83f5148b 100644 --- a/lib/core/update/version_check_service.dart +++ b/lib/core/update/version_check_service.dart @@ -8,7 +8,7 @@ import 'dart:io'; class AppVersionInfo { final String version; final int versionCode; - final String updateType; // "patch" 或 "full" + final String updateType; final bool forceUpdate; final String updateDesc; final String? patchUrl; @@ -29,13 +29,12 @@ class AppVersionInfo { }); factory AppVersionInfo.fromJson(Map json) { - // 兼容嵌套结构:如果存在 patch 对象,则从中提取 url 和 md5 final patchData = json['patch'] as Map?; final fullApkData = json['fullApk'] as Map?; return AppVersionInfo( version: json['version'] ?? '', - versionCode: json['versionCode'] ?? 0, // 处理 null 情况 + versionCode: json['versionCode'] ?? 0, updateType: json['updateType'] ?? 'full', forceUpdate: json['forceUpdate'] ?? false, updateDesc: json['updateDesc'] ?? '', @@ -59,10 +58,7 @@ class VersionCheckService { receiveTimeout: const Duration(seconds: 10), )); - // 后端接口地址 static const String apiUrl = 'http://8.159.134.0:8012/api/update/check'; - - // 🔥 补丁版本号文件路径(持久化存储,不会被清理缓存删除) static const String _patchVersionFileName = '.patch_version'; /// 检查版本更新 @@ -74,18 +70,19 @@ class VersionCheckService { _logger.i('🔍 检查版本更新'); _logger.i('📱 APK 自带版本: $currentVersion ($currentVersionCode)'); - // 🔥 从文件读取已应用的补丁版本 + // 从文件读取已应用的补丁版本 final patchInfo = await _loadPatchVersionInfo(); final appliedPatchVersion = patchInfo['version'] as String?; final appliedPatchVersionCode = patchInfo['versionCode'] as int?; + + _logger.i('📂 读取到的补丁版本: $appliedPatchVersion ($appliedPatchVersionCode)'); - // 如果打过补丁,使用补丁的版本信息;否则使用 APK 自带的版本 + // 如果打过补丁,使用补丁的版本;否则使用 APK 自带版本 final requestVersion = appliedPatchVersion ?? currentVersion; final requestVersionCode = appliedPatchVersionCode ?? currentVersionCode; _logger.i('🚀 请求后端版本: $requestVersion ($requestVersionCode)'); - // 调用后端接口 final response = await _dio.get( apiUrl, queryParameters: { @@ -104,9 +101,15 @@ class VersionCheckService { final versionInfo = AppVersionInfo.fromJson(data); - // 如果已应用此版本补丁,跳过更新 - if (appliedPatchVersion == versionInfo.version && versionInfo.updateType == 'patch') { - _logger.i('✅ 已应用最新版本补丁: ${versionInfo.version}'); + // 🔥 关键修复:无论更新类型是什么,只要本地已应用此版本,就跳过 + if (appliedPatchVersion != null && appliedPatchVersion == versionInfo.version) { + _logger.i('✅ 本地已应用版本 ${versionInfo.version},跳过更新(updateType: ${versionInfo.updateType})'); + return null; + } + + // 🔥 额外检查:如果本地补丁版本code >= 服务端返回的versionCode,也跳过 + if (appliedPatchVersionCode != null && appliedPatchVersionCode >= versionInfo.versionCode) { + _logger.i('✅ 本地补丁版本code ($appliedPatchVersionCode) >= 服务端versionCode (${versionInfo.versionCode}),跳过更新'); return null; } @@ -135,8 +138,8 @@ class VersionCheckService { }) async { try { _logger.i('⬇️ 开始下载补丁: $patchUrl'); + _logger.i('🎯 目标版本: $version ($targetVersionCode)'); - // 1. 手动下载补丁文件 final tempDir = await getTemporaryDirectory(); final patchPath = '${tempDir.path}/update_patch.so'; @@ -152,7 +155,6 @@ class VersionCheckService { _logger.i('✅ 补丁下载完成,开始应用...'); - // 2. 应用本地补丁文件 final result = await FlutterPatcher.applyPatch( PatchInfo( version: version, @@ -163,10 +165,13 @@ class VersionCheckService { ); if (result.ok) { - _logger.i('✅ 补丁应用成功,需要冷启动生效'); - // 🔥 保存补丁版本号到文件(持久化存储) + _logger.i('✅ 补丁应用成功'); + // 保存补丁版本号到文件 await _savePatchVersionInfo(version, targetVersionCode); - _logger.i('💾 已保存补丁版本: $version ($targetVersionCode)'); + + // 验证保存 + final verifyInfo = await _loadPatchVersionInfo(); + _logger.i('💾 已保存并验证: ${verifyInfo['version']} (${verifyInfo['versionCode']})'); } else { _logger.e('❌ 补丁应用失败: ${result.error}'); } @@ -183,8 +188,10 @@ class VersionCheckService { try { _logger.i('⬇️ 开始下载 APK: $apkUrl'); - final tempDir = await getTemporaryDirectory(); - final apkPath = '${tempDir.path}/app-update.apk'; + final cacheDir = await getApplicationCacheDirectory(); + final apkPath = '${cacheDir.path}/app-update.apk'; + + _logger.i('📁 APK 下载路径: $apkPath'); await _dio.download( apkUrl, @@ -209,13 +216,13 @@ class VersionCheckService { try { _logger.i('🔄 执行回滚操作'); await FlutterPatcher.rollback(); - _logger.i('✅ 回滚成功,下次冷启动生效'); + _logger.i('✅ 回滚成功'); } catch (e) { _logger.e('❌ 回滚失败: $e'); } } - // 🔥 清除补丁版本信息(整包更新后调用) + /// 清除补丁版本信息(整包更新后调用) Future clearPatchVersionInfo() async { try { final directory = await getApplicationSupportDirectory(); @@ -229,36 +236,54 @@ class VersionCheckService { } } - // 🔥 保存补丁版本信息到文件(持久化,不会被清理缓存删除) + /// 保存补丁版本信息到文件 Future _savePatchVersionInfo(String version, int versionCode) async { try { final directory = await getApplicationSupportDirectory(); final file = File('${directory.path}/$_patchVersionFileName'); - await file.writeAsString('$version|$versionCode'); + final content = '$version|$versionCode'; + await file.writeAsString(content); + + // 🔥 立即验证写入 + final verifyContent = await file.readAsString(); _logger.i('💾 补丁版本已保存到: ${file.path}'); + _logger.i('💾 写入内容: $content'); + _logger.i('💾 验证读取: $verifyContent'); + _logger.i('💾 文件存在: ${await file.exists()}'); } catch (e) { _logger.e('❌ 保存补丁版本信息失败: $e'); } } - // 🔥 从文件加载补丁版本信息 + /// 从文件加载补丁版本信息 Future> _loadPatchVersionInfo() async { try { final directory = await getApplicationSupportDirectory(); final file = File('${directory.path}/$_patchVersionFileName'); + + _logger.i('📂 尝试加载补丁版本文件: ${file.path}'); + _logger.i('📂 文件存在: ${await file.exists()}'); + if (await file.exists()) { final content = await file.readAsString(); + _logger.i('📂 文件内容: $content'); + final parts = content.split('|'); if (parts.length == 2) { final version = parts[0]; final versionCode = int.tryParse(parts[1]); if (versionCode != null) { - _logger.i('📂 从文件读取补丁版本: $version ($versionCode)'); + _logger.i('📂 解析成功: version=$version, versionCode=$versionCode'); return {'version': version, 'versionCode': versionCode}; + } else { + _logger.e('📂 versionCode 解析失败: ${parts[1]}'); } + } else { + _logger.e('📂 文件格式错误,parts.length=${parts.length}'); } } + _logger.i('📂 未找到有效的补丁版本信息'); return {'version': null, 'versionCode': null}; } catch (e) { _logger.e('❌ 加载补丁版本信息失败: $e'); diff --git a/lib/features/auth/data/models/user_model.dart b/lib/features/auth/data/models/user_model.dart index d37729bf..491572b5 100644 --- a/lib/features/auth/data/models/user_model.dart +++ b/lib/features/auth/data/models/user_model.dart @@ -8,6 +8,7 @@ class UserModel extends UserEntity implements BaseModel { required super.username, required super.nickname, required super.token, + required super.orgId, super.avatar, super.email, super.phone, @@ -20,6 +21,7 @@ class UserModel extends UserEntity implements BaseModel { username: json['username'], nickname: json['nickName'], token: json['token'], + orgId: json['orgId'] ?? 0, // 从登录响应中获取 orgId avatar: json['avatar'], email: json['email'], phone: json['phone'], @@ -32,6 +34,7 @@ class UserModel extends UserEntity implements BaseModel { 'username': username, 'nickName': nickname, 'token': token, + 'orgId': orgId, 'avatar': avatar, 'email': email, 'phone': phone, @@ -44,6 +47,7 @@ class UserModel extends UserEntity implements BaseModel { username: username, nickname: nickname, token: token, + orgId: orgId, avatar: avatar, email: email, phone: phone, @@ -56,6 +60,7 @@ class UserModel extends UserEntity implements BaseModel { username: entity.username, nickname: entity.nickname, token: entity.token, + orgId: entity.orgId, avatar: entity.avatar, email: entity.email, phone: entity.phone, diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index ae2f3fff..b62c10c6 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -19,6 +19,7 @@ import '../../../devices/presentation/bloc/devices_cubit.dart'; import '../../../devices/presentation/bloc/device_status_bloc.dart'; import '../../../devices/presentation/bloc/device_status_event.dart'; import '../../../devices/presentation/bloc/devices_state.dart'; +import '../../../../features/v2/site/presentation/cubit/site_cubit.dart'; import '../../data/datasources/auth_tcp_datasource.dart'; import '../../data/datasources/impl/auth_tcp_datasource_impl.dart'; import 'auth_state.dart'; @@ -45,23 +46,30 @@ class AuthCubit extends Cubit { /// App 启动时检查本地缓存 Future appStarted() async { final logger = GetIt.I() as SentryLoggerImpl; - // print("App 启动时检查本地缓存"); - final user = await storage.getUser(); - logger.logWithLevel( - '启动时检查本地缓存', - level: 'INFO', - data: {'user': user, 'data': user} - ); - if (user != null) { - await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); - // await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数 - tcp.startHeartbeat(interval: const Duration(seconds: 4)); - // 2. 同步全局 App 状态 - appCubit.setAuth(user); - // 3. 进入已登录状态 - emit(AuthAuthenticated(user)); - } else { - // print("App 启动时检查本地缓存user=null) "); + try { + final user = await storage.getUser(); + logger.logWithLevel( + '启动时检查本地缓存', + level: 'INFO', + data: {'user': user != null ? '找到用户: ${user.username}' : '未找到用户'} + ); + + if (user != null) { + // await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); + // await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数 + // tcp.startHeartbeat(interval: const Duration(seconds: 4)); + + // 2. 同步全局 App 状态 + appCubit.setAuth(user); + // 3. 进入已登录状态 + emit(AuthAuthenticated(user)); + logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态', level: 'INFO'); + } else { + logger.logWithLevel('⚠️ [AUTH] 应用启动 - 无本地缓存,进入未登录状态', level: 'INFO'); + emit(AuthUnauthenticated()); + } + } catch (e) { + logger.logWithLevel('❌ [AUTH] 应用启动检查失败: $e', level: 'ERROR'); emit(AuthUnauthenticated()); } } @@ -69,9 +77,9 @@ class AuthCubit extends Cubit { /// 当 HTTP 登录/注册成功后调用 Future loginSuccess(UserEntity user) async { await storage.saveUser(user); - await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); - // await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数 - //tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳 + // await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); + // await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数 + //tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳 appCubit.setAuth(user); emit(AuthAuthenticated(user)); } @@ -94,6 +102,7 @@ class AuthCubit extends Cubit { try { final devicesCubit = GetIt.I(); final deviceStatusBloc = GetIt.I(); + final siteCubit = GetIt.I(); // 1. 清空设备列表和选中设备 devicesCubit.emit(const DevicesState()); @@ -105,6 +114,11 @@ class AuthCubit extends Cubit { debugPrint('✅ [AUTH] 已重置 DeviceStatusBloc 状态'); _logger.logWithLevel('✅ [AUTH] 已重置 DeviceStatusBloc 状态'); + // 3. 清空全局选中的场站 + siteCubit.clearSelectedSite(); + debugPrint('✅ [AUTH] 已清空 SiteCubit 选中状态'); + _logger.logWithLevel('✅ [AUTH] 已清空 SiteCubit 选中状态'); + debugPrint('✅ [AUTH] 所有业务状态已清空'); _logger.logWithLevel('✅ [AUTH] 所有业务状态已清空'); } catch (e) { @@ -113,22 +127,6 @@ class AuthCubit extends Cubit { } } - /// TCP 指令监听 - // void _listenToAuthResponse() { - // print('>>> [AUTH] begin指令监听: '); - // _kickOutSub?.cancel(); // 防止重复监听 - // - // // 假设 0x12 是踢下线或多设备登录提醒 - // _kickOutSub = dispatcher.onJsonMessage(0x12).listen((json) { - // // 如果后端发来指令确认需要退出 - // print('>>> [AUTH] 收到 0x12: $json'); - // // logout(); - // final respond = json['respond'] ?? ''; - // if (respond == 'have_logged_in') { - // logout(); // ✅ 只有特定消息才退出 - // } - // }); - // } /// TCP 指令监听 void _listenToAuthResponse() { _logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'DEBUG'); diff --git a/lib/features/auth/presentation/bloc/login_cubit.dart b/lib/features/auth/presentation/bloc/login_cubit.dart index 6439233f..07571308 100644 --- a/lib/features/auth/presentation/bloc/login_cubit.dart +++ b/lib/features/auth/presentation/bloc/login_cubit.dart @@ -21,11 +21,11 @@ class LoginCubit extends Cubit { emit(LoginLoading()); try { final result = await loginUseCase.call(LoginParams(username, password, sourceType)); - final user = sl().state.user; - final devicesCubit = sl(); - if (user != null) { - devicesCubit.fetchAllDevices(user.username); - } + // final user = sl().state.user; + // final devicesCubit = sl(); + // if (user != null) { + // devicesCubit.fetchAllDevices(user.username); + // } result.fold((failure) => emit(LoginFailure(failure.message)), (user) { emit(LoginSuccess(user)); authCubit.loginSuccess(user); diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index 2f053160..6385764d 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -38,8 +38,11 @@ class _LoginPageState extends State { appBar: AppBar(backgroundColor: Colors.white, elevation: 0), body: BlocListener( listener: (context, state) { + debugPrint('🔍 [LOGIN] 登录状态变化: $state'); if (state is LoginSuccess) { + debugPrint('✅ [LOGIN] 登录成功,准备跳转到首页'); context.go(RoutePaths.home); + debugPrint('✅ [LOGIN] 已调用 context.go(RoutePaths.home)'); } else if (state is LoginFailure) { // 可以在这里弹出简洁的黑白提示框 ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败,${state.message}'))); @@ -89,7 +92,7 @@ class _LoginPageState extends State { const SizedBox(height: 16), const Text( - "账号登录v1.1.4", + "账号登录v1.1.5", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.brown), ), const SizedBox(height: 8), diff --git a/lib/features/v2/device_list/presentation/pages/device_status_page.dart b/lib/features/v2/device_list/presentation/pages/device_status_page.dart index 8b3b678f..f24fa30a 100644 --- a/lib/features/v2/device_list/presentation/pages/device_status_page.dart +++ b/lib/features/v2/device_list/presentation/pages/device_status_page.dart @@ -2,9 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../../core/di/injection.dart'; +import '../../../../../core/app/app_user_cubit.dart'; +import '../../../../v2/site/presentation/cubit/site_cubit.dart'; import '../bloc/device_status_bloc.dart'; import '../bloc/device_status_event.dart'; import '../bloc/device_status_state.dart'; +import '../bloc/drone_station_bloc.dart'; +import '../bloc/drone_station_event.dart'; +import '../bloc/drone_station_state.dart'; import '../widgets/device_item_widget.dart'; import '../widgets/drone_station_item_card.dart'; import 'robot_list_page.dart'; @@ -318,31 +323,122 @@ class DeviceStatusView extends StatelessWidget { } Widget _buildDroneStationList(BuildContext context) { - // 模拟机场数据 - final stations = [ - {'name': '1号无人机机场', 'id': 'AIRPORT-01'}, - {'name': '2号无人机机场', 'id': 'AIRPORT-02'}, - {'name': '3号无人机机场', 'id': 'AIRPORT-03'}, - ]; + // 从全局 SiteCubit 获取选中的场站 ID + final selectedSite = sl().state.selectedSite; + + if (selectedSite == null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.location_off, + size: 48, + color: Color(0xFF86909C), + ), + const SizedBox(height: 16), + const Text( + '请先选择场站', + style: TextStyle( + fontSize: 14, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ); + } - return ListView.builder( - padding: const EdgeInsets.only(top: 16, bottom: 16), - itemCount: stations.length, - itemBuilder: (context, index) { - final station = stations[index]; - return DroneStationItemCard( - name: station['name'] as String, - id: station['id'] as String, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DroneStationDetailPage(), + return BlocProvider( + create: (_) => sl()..add(DroneStationLoadData(selectedSite.id)), + child: BlocBuilder( + builder: (context, state) { + if (state is DroneStationLoading) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xFF165DFF), ), ); - }, - ); - }, + } + + if (state is DroneStationError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 48, + color: Color(0xFF86909C), + ), + const SizedBox(height: 16), + Text( + state.message, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF4E5969), + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add(DroneStationLoadData(selectedSite.id)); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF165DFF), + foregroundColor: Colors.white, + ), + child: const Text('重试'), + ), + ], + ), + ); + } + + if (state is DroneStationLoaded) { + final stations = state.stations; + + if (stations.isEmpty) { + return const Center( + child: Text( + '该场站暂无无人机机场', + style: TextStyle( + fontSize: 14, + color: Color(0xFF4E5969), + ), + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + context.read().add(DroneStationRefresh(selectedSite.id)); + }, + color: const Color(0xFF165DFF), + child: ListView.builder( + padding: const EdgeInsets.only(top: 16, bottom: 16), + itemCount: stations.length, + itemBuilder: (context, index) { + final station = stations[index]; + return DroneStationItemCard( + station: station, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DroneStationDetailPage(station: station), + ), + ); + }, + ); + }, + ), + ); + } + + return const Center(child: Text('暂无数据')); + }, + ), ); } diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart index dc48cd07..3ce3c0d3 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart @@ -1,111 +1,133 @@ import 'package:flutter/material.dart'; -import 'drone_mission_control_page.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../../core/di/injection.dart'; +import '../../domain/entities/drone_station_entity.dart'; +import '../bloc/drone_station_bloc.dart'; +import '../bloc/drone_station_event.dart'; +import '../bloc/drone_station_state.dart'; import 'drone_video_control_page.dart'; +import 'drone_mission_control_page.dart'; +import 'drone_monitor_page.dart'; -/// 无人机机场详情页面 -class DroneStationDetailPage extends StatelessWidget { - const DroneStationDetailPage({super.key}); +class DroneStationDetailPage extends StatefulWidget { + final DroneStationEntity station; + + const DroneStationDetailPage({ + super.key, + required this.station, + }); + + @override + State createState() => _DroneStationDetailPageState(); +} + +class _DroneStationDetailPageState extends State { + late DroneStationBloc _bloc; + + @override + void initState() { + super.initState(); + _bloc = sl(); + _bloc.add(UAVDetailLoad( + gatewaySn: widget.station.gatewaySn, + deviceSn: widget.station.deviceSn, + )); + } + + @override + void dispose() { + _bloc.close(); + super.dispose(); + } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F6F8), - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), - onPressed: () => Navigator.pop(context), - ), - title: const Text( - '无人机机场详情', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF1D2129), + return BlocProvider.value( + value: _bloc, + child: Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), ), + title: const Text( + '无人机机场详情', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + ), + body: BlocBuilder( + builder: (context, state) { + if (state is UAVDetailLoading) { + return const Center( + child: CircularProgressIndicator(color: Color(0xFF165DFF)), + ); + } + + if (state is UAVDetailError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)), + const SizedBox(height: 16), + Text(state.message, style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969))), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + _bloc.add(UAVDetailLoad( + gatewaySn: widget.station.gatewaySn, + deviceSn: widget.station.deviceSn, + )); + }, + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF165DFF)), + child: const Text('重试'), + ), + ], + ), + ); + } + + if (state is UAVDetailLoaded) { + return _buildContent(state.detail); + } + + return const Center(child: Text('正在加载...')); + }, ), - centerTitle: true, - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildAirportStatusCard(), - const SizedBox(height: 12), - _buildDroneStatusCard(context), - const SizedBox(height: 12), - _buildQuickActions(context), - const SizedBox(height: 20), - ], ), ); } - /// 机场状态卡片 - Widget _buildAirportStatusCard() { + Widget _buildContent(UAVDetailEntity detail) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildAirportStatusCard(detail), + const SizedBox(height: 12), + _buildDroneStatusCard(detail), + const SizedBox(height: 12), + _buildQuickActions(), + const SizedBox(height: 20), + ], + ); + } + + Widget _buildAirportStatusCard(UAVDetailEntity detail) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: const [ - BoxShadow( - color: Color(0x0D000000), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '机场状态', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF1D2129), - ), - ), - const SizedBox(height: 16), - _buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'), - const SizedBox(height: 12), - _buildStatusRow('位置', '升压站旁 - 无人机机场'), - const SizedBox(height: 12), - _buildStatusRow('舱门状态', '关闭', status: '正常'), - const SizedBox(height: 12), - _buildStatusRowWithProgress('充电状态', '充电中', '85%'), - const SizedBox(height: 12), - _buildWeatherRow(), - const SizedBox(height: 12), - _buildStatusRow('网络状态', '4G/5G 强'), - ], - ), - ); - } - - /// 无人机状态卡片 - Widget _buildDroneStatusCard(BuildContext context) { - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DroneVideoControlPage(), - ), - ); - }, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Color(0x0D000000), - blurRadius: 8, - offset: Offset(0, 2), - ), + BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2)), ], ), child: Column( @@ -113,148 +135,109 @@ class DroneStationDetailPage extends StatelessWidget { children: [ Row( children: [ - const Text( - '无人机状态', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF1D2129), - ), - ), + const Text('机场状态', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: const Color(0xFF00B42A).withOpacity(0.1), + color: detail.isOnline ? const Color(0xFF00B42A).withOpacity(0.1) : const Color(0xFFF53F3F).withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), - child: const Text( - '在线', - style: TextStyle( - fontSize: 12, - color: Color(0xFF00B42A), - fontWeight: FontWeight.w500, - ), - ), + child: Text(detail.isOnline ? '在线' : '离线', style: TextStyle(fontSize: 12, color: detail.isOnline ? const Color(0xFF00B42A) : const Color(0xFFF53F3F), fontWeight: FontWeight.w500)), ), ], ), const SizedBox(height: 16), - Row( - children: [ - Image.asset( - 'assets/images/xunjian.png', - width: 60, - height: 60, - fit: BoxFit.contain, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '无人机 - 01', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Color(0xFF1D2129), - ), - ), - const SizedBox(height: 8), - _buildDroneBatteryRow(), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - _buildStatusRow('飞行模式', '待命'), - const SizedBox(height: 12), - _buildStatusRow('机身状态', '正常'), - const SizedBox(height: 12), - _buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'), + const Divider(height: 1, color: Color(0xFFF2F3F5)), + const SizedBox(height: 16), + _buildInfoRow('机场名称', detail.callsign.isNotEmpty ? detail.callsign : '未知'), + _buildInfoRow('设备序列号', detail.deviceSn), + _buildInfoRow('网关序列号', detail.gatewaySn), + _buildInfoRow('位置坐标', (detail.latitude != null && detail.longitude != null) ? '${detail.latitude}, ${detail.longitude}' : '未知'), + _buildInfoRow('电量', detail.capacityPercent != null ? '${detail.capacityPercent}%' : '未知'), + _buildInfoRow('环境温度', detail.environmentTemperature != null ? '${detail.environmentTemperature}°C' : '未知'), + _buildInfoRow('风速', detail.windSpeed != null ? '${detail.windSpeed} m/s' : '未知'), + _buildInfoRow('降雨量', detail.rainfall ?? '未知'), + _buildInfoRow('网络状态', detail.networkState?.toString() ?? '未知'), + _buildPositionStateRow('位置状态', detail.positionState), ], ), - ), ); } - /// 快捷操作按钮 - Widget _buildQuickActions(BuildContext context) { + Widget _buildDroneStatusCard(UAVDetailEntity detail) { + return GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const DroneVideoControlPage())); + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text('无人机状态', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: detail.isDroneOnline ? const Color(0xFF00B42A).withOpacity(0.1) : const Color(0xFFF53F3F).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text(detail.isDroneOnline ? '在线' : '离线', style: TextStyle(fontSize: 12, color: detail.isDroneOnline ? const Color(0xFF00B42A) : const Color(0xFFF53F3F), fontWeight: FontWeight.w500)), + ), + ], + ), + const SizedBox(height: 16), + _buildInfoRow('无人机呼号', detail.droneCallsign.isNotEmpty ? detail.droneCallsign : '未知'), + const SizedBox(height: 12), + _buildInfoRow('设备序列号', detail.deviceSn), + const SizedBox(height: 12), + _buildInfoRow('电量', detail.capacityPercent != null ? '${detail.capacityPercent}%' : '未知'), + const SizedBox(height: 12), + _buildInfoRow('高度', detail.height != null ? '${detail.height} m' : '未知'), + const SizedBox(height: 12), + _buildInfoRow('距离home点', detail.homeDistance != null ? '${detail.homeDistance} m' : '未知'), + const SizedBox(height: 12), + _buildInfoRow('实时电量', detail.liveCapacity != null ? '${detail.liveCapacity}%' : '未知'), + if (detail.gatewayCameraList != null && detail.gatewayCameraList!.isNotEmpty) ...[ + const SizedBox(height: 12), + _buildCameraListRow('网关摄像头', detail.gatewayCameraList!), + ], + ], + ), + ), + ); + } + + Widget _buildQuickActions() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Color(0x0D000000), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], + boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '快捷操作', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF1D2129), - ), - ), + const Text('快捷操作', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))), const SizedBox(height: 16), Row( children: [ - Expanded( - child: _buildQuickActionButton( - context, - icon: Icons.flight_takeoff, - label: '开舱', - color: const Color(0xFF165DFF), - onTap: () {}, - ), - ), + Expanded(child: _buildQuickActionButton(icon: Icons.flight_takeoff, label: '开舱', color: const Color(0xFF165DFF), onTap: () {})), const SizedBox(width: 12), - Expanded( - child: _buildQuickActionButton( - context, - icon: Icons.flight, - label: '起飞准备', - color: const Color(0xFF165DFF), - onTap: () {}, - ), - ), + Expanded(child: _buildQuickActionButton(icon: Icons.flight, label: '起飞准备', color: const Color(0xFF165DFF), onTap: () {})), const SizedBox(width: 12), - Expanded( - child: _buildQuickActionButton( - context, - icon: Icons.home, - label: '返航', - color: const Color(0xFFFF7D00), - onTap: () {}, - ), - ), + Expanded(child: _buildQuickActionButton(icon: Icons.home, label: '返航', color: const Color(0xFFFF7D00), onTap: () {})), const SizedBox(width: 12), - Expanded( - child: _buildQuickActionButton( - context, - icon: Icons.task, - label: '任务下发', - color: const Color(0xFF165DFF), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DroneMissionControlPage(), - ), - ); - }, - ), - ), + Expanded(child: _buildQuickActionButton(icon: Icons.monitor, label: '看监控', color: const Color(0xFF165DFF), onTap: _goToMonitor)), ], ), ], @@ -262,37 +245,45 @@ class DroneStationDetailPage extends StatelessWidget { ); } - /// 快捷操作按钮 - Widget _buildQuickActionButton( - BuildContext context, { - required IconData icon, - required String label, - required Color color, - required VoidCallback onTap, - }) { + void _goToMonitor() { + Navigator.push(context, MaterialPageRoute(builder: (context) => DroneMonitorPage())); + } + + Widget _buildQuickActionButton({required IconData icon, required String label, required Color color, required VoidCallback onTap}) { return GestureDetector( onTap: onTap, child: Column( children: [ Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - icon, - color: color, - size: 24, - ), + width: 48, height: 48, + decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), + child: Icon(icon, color: color, size: 24), ), const SizedBox(height: 8), - Text( - label, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF4E5969), + Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969))), + ], + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF86909C))), + ), + const SizedBox(width: 12), + const Text(':', style: TextStyle(fontSize: 12, color: Color(0xFFC0C4CC))), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: const TextStyle(fontSize: 12, color: Color(0xFF1D2129), fontWeight: FontWeight.w500), + textAlign: TextAlign.left, ), ), ], @@ -300,166 +291,29 @@ class DroneStationDetailPage extends StatelessWidget { ); } - /// 状态行(带状态标签) - Widget _buildStatusRow(String label, String value, {String? status}) { + Widget _buildPositionStateRow(String label, PositionState? positionState) { + String value = '未知'; + if (positionState != null) { + value = 'GPS:${positionState.gpsNumber} RTX:${positionState.rtkNumber} 固定:${positionState.isFixed}'; + } return Row( children: [ - Text( - label, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF86909C), - ), - ), + Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))), const Spacer(), - Text( - value, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF1D2129), - fontWeight: FontWeight.w500, - ), - ), - if (status != null) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: const Color(0xFF00B42A).withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - status, - style: const TextStyle( - fontSize: 11, - color: Color(0xFF00B42A), - fontWeight: FontWeight.w500, - ), - ), - ), - ], + Flexible(child: Text(value, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129), fontWeight: FontWeight.w500), textAlign: TextAlign.right)), ], ); } - /// 带进度条的状态行 - Widget _buildStatusRowWithProgress(String label, String value, String progress) { + Widget _buildCameraListRow(String label, List cameras) { + String value = cameras.map((c) => '${c.cameraIndex}:${c.cameraPosition}').join(' | '); return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF86909C), - ), - ), + Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))), const Spacer(), - Text( - value, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF1D2129), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(width: 8), - Text( - progress, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF165DFF), - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 4), - const Icon( - Icons.arrow_forward_ios, - size: 12, - color: Color(0xFF86909C), - ), + Flexible(child: Text(value, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129), fontWeight: FontWeight.w500), textAlign: TextAlign.right)), ], ); } - - /// 气象条件行 - Widget _buildWeatherRow() { - return Row( - children: [ - const Text( - '气象条件', - style: TextStyle( - fontSize: 13, - color: Color(0xFF86909C), - ), - ), - const Spacer(), - Row( - children: [ - const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)), - const SizedBox(width: 4), - const Text( - '晴', - style: TextStyle( - fontSize: 13, - color: Color(0xFF1D2129), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(width: 8), - const Text( - '25°C', - style: TextStyle( - fontSize: 13, - color: Color(0xFF1D2129), - ), - ), - const SizedBox(width: 8), - const Text( - '东南风 2级', - style: TextStyle( - fontSize: 13, - color: Color(0xFF1D2129), - ), - ), - ], - ), - ], - ); - } - - /// 无人机电量行 - Widget _buildDroneBatteryRow() { - return Row( - children: [ - const Text( - '电量', - style: TextStyle( - fontSize: 13, - color: Color(0xFF86909C), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: 0.78, - backgroundColor: const Color(0xFFF2F3F5), - valueColor: const AlwaysStoppedAnimation(Color(0xFF00B42A)), - minHeight: 6, - ), - ), - ), - const SizedBox(width: 8), - const Text( - '78%', - style: TextStyle( - fontSize: 13, - color: Color(0xFF00B42A), - fontWeight: FontWeight.w600, - ), - ), - ], - ); - } -} +} \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart index 2147c1bd..8c9c8734 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart @@ -182,12 +182,12 @@ class DroneStationStatusPage extends StatelessWidget { ), ], ), - const SizedBox(height: 12), - _buildStatusRow('飞行模式', '待命'), - const SizedBox(height: 12), - _buildStatusRow('机身状态', '正常'), - const SizedBox(height: 12), - _buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'), + // const SizedBox(height: 12), + // _buildStatusRow('飞行模式', '待命'), + // const SizedBox(height: 12), + // _buildStatusRow('机身状态', '正常'), + // const SizedBox(height: 12), + // _buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'), ], ), ); diff --git a/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart index cc2f3e75..c9daf08e 100644 --- a/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart +++ b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart @@ -1,12 +1,19 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; +import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/site_entity.dart'; import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_home_data_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_site_list_usecase.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart'; class HomeV2Bloc extends Bloc { final GetHomeDataUseCase getHomeDataUseCase; + final GetSiteListUseCase getSiteListUseCase; + final AppUserCubit appUserCubit; + final SiteCubit siteCubit; - HomeV2Bloc(this.getHomeDataUseCase) : super(const HomeV2Initial()) { + HomeV2Bloc(this.getHomeDataUseCase, this.getSiteListUseCase, this.appUserCubit, this.siteCubit) : super(const HomeV2Initial()) { on(_onLoadData); on(_onRefresh); on(_onToggleTrendType); @@ -18,11 +25,55 @@ class HomeV2Bloc extends Bloc { ) async { emit(const HomeV2Loading()); - final result = await getHomeDataUseCase(const NoParams()); + final user = appUserCubit.state.user; + if (user == null) { + emit(const HomeV2Error('用户未登录')); + return; + } - result.fold( + // 并行加载首页数据和场站列表 + final homeResult = await getHomeDataUseCase(const NoParams()); + final siteResult = await getSiteListUseCase(user.orgId); // 使用用户的 orgId + + homeResult.fold( (failure) => emit(HomeV2Error(failure.message)), - (data) => emit(HomeV2Loaded(homeData: data)), + (homeData) { + List sites = []; + SiteEntity? selectedSite; + + siteResult.fold( + (failure) { + print('加载场站列表失败: ${failure.message}'); + }, + (siteList) { + sites = siteList; + // 从全局 SiteCubit 获取之前选中的场站 + final savedSelectedSite = siteCubit.state.selectedSite; + + // 尝试找到之前选中的场站 + if (savedSelectedSite != null && siteList.isNotEmpty) { + selectedSite = siteList.firstWhere( + (site) => site.id == savedSelectedSite.id, + orElse: () => siteList.first, + ); + } else { + // 没有选中过,默认选中第一个 + selectedSite = siteList.isNotEmpty ? siteList.first : null; + } + + // 更新全局 SiteCubit 的选中状态 + if (selectedSite != null) { + siteCubit.selectSite(selectedSite!); + } + }, + ); + + emit(HomeV2Loaded( + homeData: homeData, + sites: sites, + selectedSite: selectedSite, + )); + }, ); } @@ -31,14 +82,20 @@ class HomeV2Bloc extends Bloc { Emitter emit, ) async { if (state is HomeV2Loaded) { - final result = await getHomeDataUseCase(const NoParams()); + final currentState = state as HomeV2Loaded; + + final homeResult = await getHomeDataUseCase(const NoParams()); - result.fold( + homeResult.fold( (failure) => emit(HomeV2Error(failure.message)), - (data) => emit(HomeV2Loaded( - homeData: data, - trendType: (state as HomeV2Loaded).trendType, - )), + (homeData) { + emit(HomeV2Loaded( + homeData: homeData, + trendType: currentState.trendType, + sites: currentState.sites, + selectedSite: currentState.selectedSite, + )); + }, ); } } diff --git a/lib/features/v2/home/presentation/bloc/home_v2_event.dart b/lib/features/v2/home/presentation/bloc/home_v2_event.dart index a77b0511..0b0c2efe 100644 --- a/lib/features/v2/home/presentation/bloc/home_v2_event.dart +++ b/lib/features/v2/home/presentation/bloc/home_v2_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/site_entity.dart'; abstract class HomeV2Event extends Equatable { const HomeV2Event(); @@ -23,3 +24,12 @@ class HomeV2ToggleTrendType extends HomeV2Event { @override List get props => [type]; } + +class HomeV2SelectSite extends HomeV2Event { + final SiteEntity site; + + const HomeV2SelectSite(this.site); + + @override + List get props => [site]; +} diff --git a/lib/features/v2/home/presentation/bloc/home_v2_state.dart b/lib/features/v2/home/presentation/bloc/home_v2_state.dart index 8fea8275..d37ca36f 100644 --- a/lib/features/v2/home/presentation/bloc/home_v2_state.dart +++ b/lib/features/v2/home/presentation/bloc/home_v2_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/site_entity.dart'; abstract class HomeV2State extends Equatable { const HomeV2State(); @@ -19,24 +20,32 @@ class HomeV2Loading extends HomeV2State { class HomeV2Loaded extends HomeV2State { final HomeEntity homeData; final String trendType; + final List sites; + final SiteEntity? selectedSite; const HomeV2Loaded({ required this.homeData, this.trendType = 'kW', + this.sites = const [], + this.selectedSite, }); HomeV2Loaded copyWith({ HomeEntity? homeData, String? trendType, + List? sites, + SiteEntity? selectedSite, }) { return HomeV2Loaded( homeData: homeData ?? this.homeData, trendType: trendType ?? this.trendType, + sites: sites ?? this.sites, + selectedSite: selectedSite ?? this.selectedSite, ); } @override - List get props => [homeData, trendType]; + List get props => [homeData, trendType, sites, selectedSite]; } class HomeV2Error extends HomeV2State { diff --git a/lib/features/v2/home/presentation/pages/home_v2_page.dart b/lib/features/v2/home/presentation/pages/home_v2_page.dart index 668f8821..1a670892 100644 --- a/lib/features/v2/home/presentation/pages/home_v2_page.dart +++ b/lib/features/v2/home/presentation/pages/home_v2_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart'; +import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_card.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/stats_grid.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/work_order_card.dart'; @@ -19,13 +21,6 @@ class HomeV2Page extends StatefulWidget { class _HomeV2PageState extends State { late HomeV2Bloc _bloc; - String _selectedPlant = '示例光伏电站'; - final List _plantList = [ - '示例光伏电站', - '一号光伏电站', - '二号光伏电站', - '三号光伏电站', - ]; @override void initState() { @@ -82,15 +77,21 @@ class _HomeV2PageState extends State { mainAxisSize: MainAxisSize.min, children: [ Flexible( - child: Text( - _selectedPlant, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF1D2129), - ), + child: StreamBuilder( + stream: sl().stream, + builder: (context, snapshot) { + final siteState = snapshot.data ?? sl().state; + return Text( + siteState.selectedSite?.siteName ?? '选择电站', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ); + }, ), ), const SizedBox(width: 4), @@ -210,6 +211,18 @@ class _HomeV2PageState extends State { } void _showPlantSelector() { + if (_bloc.state is! HomeV2Loaded) return; + + final currentState = _bloc.state as HomeV2Loaded; + final sites = currentState.sites; + + if (sites.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('暂无可用电站')), + ); + return; + } + showModalBottomSheet( context: context, backgroundColor: Colors.white, @@ -217,34 +230,60 @@ class _HomeV2PageState extends State { borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) { - return Container( - padding: const EdgeInsets.all(14), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - '选择电站', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF1D2129), - ), + return BlocBuilder( + bloc: _bloc, + builder: (context, state) { + if (state is! HomeV2Loaded) return Container(); + + return Container( + padding: const EdgeInsets.all(14), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.6, ), - const SizedBox(height: 14), - ..._plantList.map((plant) => ListTile( - title: Text(plant), - trailing: _selectedPlant == plant - ? const Icon(Icons.check, color: Color(0xFF165DFF)) - : null, - onTap: () { - setState(() { - _selectedPlant = plant; - }); - Navigator.pop(context); - }, - )), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '选择电站', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 14), + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: state.sites.length, + itemBuilder: (context, index) { + final site = state.sites[index]; + final isSelected = state.selectedSite?.id == site.id; + + return ListTile( + title: Text(site.siteName), + subtitle: site.siteCode != null && site.siteCode!.isNotEmpty + ? Text('编号: ${site.siteCode}') + : null, + trailing: isSelected + ? const Icon(Icons.check, color: Color(0xFF165DFF)) + : null, + onTap: () { + // 更新全局选中的场站 + sl().selectSite(site); + Navigator.pop(context); + + // 切换场站后,重新加载首页数据 + _bloc.add(const HomeV2LoadData()); + }, + ); + }, + ), + ), + ], + ), + ); + }, ); }, ); diff --git a/lib/features/v2/my/presentation/pages/my_page.dart b/lib/features/v2/my/presentation/pages/my_page.dart index 48c985d0..500e728c 100644 --- a/lib/features/v2/my/presentation/pages/my_page.dart +++ b/lib/features/v2/my/presentation/pages/my_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/services.dart'; +import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/cubit/my_cubit.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/states/my_state.dart'; import 'package:maibu_satabot_v2/features/v2/my/di/my_di.dart'; @@ -9,6 +10,7 @@ import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/user_profil import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/quick_action_card.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/menu_item_card.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/system_settings_page.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/profile_detail_page.dart'; /// 我的页面(100% 还原设计稿) class MyPage extends StatelessWidget { @@ -53,15 +55,28 @@ class _MyPageContent extends StatelessWidget { if (state is MyLoaded) { final cubit = context.read(); + final appUserState = context.watch().state; + final user = appUserState.user; + + // 获取用户信息(优先使用昵称,其次用户名) + final userName = user?.nickname ?? user?.username ?? '未知用户'; + final userRole = user?.email ?? user?.phone ?? '未设置'; + return Column( children: [ // 用户信息卡片(延伸到状态栏) UserProfileCard( - name: state.userProfile.name, - role: state.userProfile.role, + name: userName, + role: userRole, + avatar: user?.avatar, onTap: () { - // TODO: 跳转到个人信息页面 - debugPrint('点击个人信息'); + // 跳转到个人信息详情页 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ProfileDetailPage(), + ), + ); }, ), // 快捷入口卡片(负margin向上重叠) @@ -125,7 +140,19 @@ class _MyPageContent extends StatelessWidget { /// 处理菜单项点击 void _handleMenuItemTap(BuildContext context, MyCubit cubit, dynamic item) { + debugPrint('🔍 点击菜单项 ID: ${item.id}, 标题: ${item.title}'); switch (item.id) { + case 'personal_info': + debugPrint('✅ 准备跳转到个人信息详情页'); + // 跳转到个人信息详情页 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ProfileDetailPage(), + ), + ); + debugPrint('✅ 已调用 Navigator.push'); + break; case 'offline_cache': _showClearCacheDialog(context, cubit); break; diff --git a/lib/features/v2/my/presentation/widgets/user_profile_card.dart b/lib/features/v2/my/presentation/widgets/user_profile_card.dart index 6706180a..fee3012d 100644 --- a/lib/features/v2/my/presentation/widgets/user_profile_card.dart +++ b/lib/features/v2/my/presentation/widgets/user_profile_card.dart @@ -7,92 +7,110 @@ class UserProfileCard extends StatelessWidget { super.key, required this.name, required this.role, + this.avatar, this.onTap, }); final String name; final String role; + final String? avatar; final VoidCallback? onTap; @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(20, 70, 20, 50), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFF165DFF), - Color(0xFF0E42CC), + return InkWell( + onTap: onTap, + child: Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(20, 70, 20, 50), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF165DFF), + Color(0xFF0E42CC), + ], + ), + ), + child: Row( + children: [ + // 头像 + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + child: ClipOval( + child: _buildAvatar(), + ), + ), + const SizedBox(width: 14), + // 用户信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + Text( + role, + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(0.85), + letterSpacing: 0.3, + ), + ), + ], + ), + ), + // 箭头 + Icon( + Icons.chevron_right, + color: Colors.white.withOpacity(0.9), + size: 24, + ), ], ), ), - child: Row( - children: [ - // 头像 - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 2, - ), - ), - child: ClipOval( - child: Image.asset( - 'assets/images/app_logo.png', - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.white, - child: Icon( - Icons.person, - size: 32, - color: const Color(0xFF165DFF), - ), - ); - }, - ), - ), - ), - const SizedBox(width: 14), - // 用户信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 6), - Text( - role, - style: TextStyle( - fontSize: 13, - color: Colors.white.withOpacity(0.85), - letterSpacing: 0.3, - ), - ), - ], - ), - ), - // 箭头 - Icon( - Icons.chevron_right, - color: Colors.white.withOpacity(0.9), - size: 24, - ), - ], + ); + } + + Widget _buildAvatar() { + // 如果有头像 URL,显示网络图片 + if (avatar != null && avatar!.isNotEmpty) { + return Image.network( + avatar!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _buildDefaultAvatar(); + }, + ); + } + // 否则显示默认头像 + return _buildDefaultAvatar(); + } + + Widget _buildDefaultAvatar() { + return Container( + color: Colors.white, + child: Icon( + Icons.person, + size: 32, + color: const Color(0xFF165DFF), ), ); } diff --git a/lib/main.dart b/lib/main.dart index bcda0e71..c1169b89 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,6 +41,11 @@ void main() async { final logger = sl(); await logger.init(); Bloc.observer = AppBlocObserver(logger); + + // ⚠️ 注意:不要在启动时清除补丁版本记录! + // 补丁版本记录只在整包更新成功后才清除 + // 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新 + runApp(const MyApp()); }, (error, stack) { @@ -203,6 +208,33 @@ class _UpdateCheckerState extends State<_UpdateChecker> { Text(state.versionInfo.updateDesc), ], const SizedBox(height: 20), + if (state.versionInfo.updateType != 'patch') ...[ + // 🔥 整包更新:显示两个选项 + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '💡 选择下载方式', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + const SizedBox(height: 8), + const Text( + '• 自动安装:App 内下载并调起系统安装\n' + '• 手动下载:在浏览器中下载安装', + style: TextStyle(fontSize: 13, height: 1.5), + ), + ], + ), + ), + const SizedBox(height: 16), + ], Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -212,24 +244,44 @@ class _UpdateCheckerState extends State<_UpdateChecker> { child: const Text('稍后'), ), const SizedBox(width: 10), - ElevatedButton( - onPressed: () { - final info = state.versionInfo; - if (info.patchUrl != null) { - // 🔥 执行差量更新 - context.read().applyPatch( - info.patchUrl!, - info.version, - info.versionCode, - md5: info.patchMd5, - ); - } else if (info.apkUrl != null) { - // 🔥 执行整包更新 - context.read().downloadAndInstallApk(info.apkUrl!); - } - }, - child: const Text('立即更新'), - ), + if (state.versionInfo.updateType == 'patch') + // 🔥 差量更新:单个按钮 + ElevatedButton( + onPressed: () { + final info = state.versionInfo; + if (info.patchUrl != null) { + context.read().applyPatch( + info.patchUrl!, + info.version, + info.versionCode, + md5: info.patchMd5, + ); + } + }, + child: const Text('立即更新'), + ) + else ...[ + // 🔥 整包更新:两个按钮 + OutlinedButton( + onPressed: () { + final info = state.versionInfo; + if (info.apkUrl != null) { + context.read().downloadAndInstallApk(info.apkUrl!); + } + }, + child: const Text('手动下载'), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + final info = state.versionInfo; + if (info.apkUrl != null) { + context.read().downloadAndInstallApk(info.apkUrl!); + } + }, + child: const Text('自动安装'), + ), + ], ], ), ], @@ -239,8 +291,49 @@ class _UpdateCheckerState extends State<_UpdateChecker> { ), ), ), - // 🔥 下载/安装进度弹窗 - if (state is UpdateDownloading || state is UpdateInstalling) + // 🔥 整包更新步骤引导弹窗 + if (state is UpdateInstalling && !(state.isPatch ?? false)) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.7), + child: Center( + child: Card( + margin: const EdgeInsets.symmetric(horizontal: 30), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '📱 整包更新步骤', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + _buildStepItem(1, '在浏览器中下载 APK', true), + const SizedBox(height: 8), + _buildStepItem(2, '手动安装下载的 APK', false), + const SizedBox(height: 8), + _buildStepItem(3, '清除旧版本记录', false), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + context.read().cancelUpdate(); + }, + child: const Text('我知道了'), + ), + ), + ], + ), + ), + ), + ), + ), + ), + // 🔥 下载进度显示(差量补丁和整包APK) + if (state is UpdateDownloading) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.7), @@ -252,31 +345,32 @@ class _UpdateCheckerState extends State<_UpdateChecker> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (state is UpdateDownloading) - const Text('⬇️ 正在下载更新...', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - if (state is UpdateInstalling) - const Text('🔧 正在应用更新...', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 16), - if (state is UpdateDownloading) - LinearProgressIndicator( - value: state.progress, - minHeight: 8, - borderRadius: BorderRadius.circular(4), + Text( + state.isPatch ? '⬇️ 正在下载补丁...' : '⬇️ 正在下载 APK...', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + SizedBox( + width: 80, + height: 80, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: state.progress, + strokeWidth: 5, + ), + Text( + '${(state.progress * 100).toInt()}%', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], ), - if (state is UpdateDownloading) - const SizedBox(height: 8), - if (state is UpdateDownloading) - Text('${(state.progress * 100).toInt()}%', - style: const TextStyle(fontSize: 14, color: Colors.grey)), - if (state is UpdateInstalling) - const CircularProgressIndicator(), + ), const SizedBox(height: 16), Text( - state is UpdateDownloading ? '请稍候,正在下载补丁文件' : '正在应用补丁,请勿关闭应用', - style: const TextStyle(fontSize: 14, color: Colors.grey), - textAlign: TextAlign.center, + '请稍候...', + style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), ], ), @@ -358,4 +452,42 @@ class _UpdateCheckerState extends State<_UpdateChecker> { }, ); } + + /// 构建步骤项 + Widget _buildStepItem(int step, String text, bool completed) { + return Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: completed ? Colors.green : Colors.grey.shade300, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '$step', + style: TextStyle( + color: completed ? Colors.white : Colors.black54, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 15, + color: completed ? Colors.green : Colors.black87, + decoration: completed ? TextDecoration.lineThrough : null, + ), + ), + ), + if (completed) + const Icon(Icons.check, color: Colors.green, size: 20), + ], + ); + } }