修复版本管理整包更新无法弹出的问题
完成集成新的登录接口的更换使用 完成集成用户的场站列表接口 完成开发设计选择场站项为全局属性 完成我的页面的个人信息的集成和详情页面的开发 完成设备页面的中无人机机场的列表接口对接入和个项页面的布局的优化 完成设备页面的中无人机机场的详情接口的对接和使用和页面的更新布局
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
@@ -11,24 +9,19 @@
|
||||
<!-- 存储权限 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- 🔥 允许安装 APK -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- 关键! -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- 如果还需要扫描闪光灯控制,可选添加 -->
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
|
||||
<application
|
||||
android:label="飒沓机器人"
|
||||
android:name="${applicationName}"
|
||||
@@ -47,10 +40,6 @@
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
@@ -61,25 +50,15 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<!-- 高德 Key -->
|
||||
<meta-data
|
||||
android:name="com.amap.api.v2.apikey"
|
||||
android:value="bbb1f0f20eed6bf679eddf2625630aba" />
|
||||
|
||||
|
||||
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
|
||||
9
dist/version/check.json
vendored
Normal file
9
dist/version/check.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"hasUpdate": true,
|
||||
"patch": {
|
||||
"version": "1.0.2",
|
||||
"patchUrl": "http://127.0.0.1:8080/libapp.so",
|
||||
"md5": "",
|
||||
"targetVersionCode": 1
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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<void> init() async {
|
||||
sl.registerLazySingleton<HomeRemoteDataSource>(() => HomeRemoteDataSourceImpl());
|
||||
sl.registerLazySingleton<HomeRepository>(() => HomeRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetHomeDataUseCase>(() => GetHomeDataUseCase(sl()));
|
||||
sl.registerFactory<HomeV2Bloc>(() => HomeV2Bloc(sl()));
|
||||
|
||||
// Site (场站)
|
||||
sl.registerLazySingleton<SiteDataSource>(() => SiteDataSourceImpl(sl<Dio>()));
|
||||
sl.registerLazySingleton<SiteRepository>(() => SiteRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetSiteListUseCase>(() => GetSiteListUseCase(sl()));
|
||||
|
||||
sl.registerFactory<HomeV2Bloc>(() => HomeV2Bloc(sl(), sl(), sl<AppUserCubit>(), sl<SiteCubit>()));
|
||||
|
||||
/// Device Status V2
|
||||
sl.registerLazySingleton<device_v2.DeviceRemoteDataSource>(() => device_v2.DeviceRemoteDataSourceImpl());
|
||||
@@ -212,6 +230,13 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<device_v2_usecase.GetDeviceStatusDataUseCase>(() => device_v2_usecase.GetDeviceStatusDataUseCase(repository: sl()));
|
||||
sl.registerFactory<device_v2_bloc.DeviceStatusBloc>(() => device_v2_bloc.DeviceStatusBloc(sl()));
|
||||
|
||||
/// Drone Station V2
|
||||
sl.registerLazySingleton<DroneStationDataSource>(() => DroneStationDataSourceImpl(sl<Dio>()));
|
||||
sl.registerLazySingleton<DroneStationRepository>(() => DroneStationRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetDroneStationListUseCase>(() => GetDroneStationListUseCase(sl()));
|
||||
sl.registerLazySingleton<GetUAVDetailUseCase>(() => GetUAVDetailUseCase(sl()));
|
||||
sl.registerFactory<DroneStationBloc>(() => DroneStationBloc(sl(), sl()));
|
||||
|
||||
/// Alarm Center V2
|
||||
sl.registerLazySingleton<AlarmRemoteDataSource>(() => AlarmRemoteDataSourceImpl());
|
||||
sl.registerLazySingleton<AlarmRepository>(() => AlarmRepositoryImpl(sl()));
|
||||
@@ -226,6 +251,9 @@ Future<void> init() async {
|
||||
/// 5. 状态管理 (Cubit/Bloc)
|
||||
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
|
||||
|
||||
// 🔥 SiteCubit (全局共享,持久化选中场站)
|
||||
sl.registerLazySingleton(() => SiteCubit(sl<SharedPreferences>()));
|
||||
|
||||
// 🔥 语言管理 Cubit (单例)
|
||||
sl.registerLazySingleton(() => LocaleCubit());
|
||||
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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<UpdateState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载并安装完整 APK
|
||||
/// 自动下载并安装 APK(App 内下载)
|
||||
Future<void> downloadAndInstallApk(String apkUrl) async {
|
||||
emit(const UpdateDownloading(0.0, isPatch: false));
|
||||
|
||||
@@ -93,16 +96,19 @@ class UpdateCubit extends Cubit<UpdateState> {
|
||||
});
|
||||
|
||||
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<UpdateState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 触发系统安装页面
|
||||
Future<void> 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<void> 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('❌ 用户取消更新');
|
||||
|
||||
@@ -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<UpdateCubit, UpdateState>(
|
||||
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<UpdateCubit, UpdateState>(
|
||||
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<UpdateCubit>().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<UpdateCubit>().applyPatch(
|
||||
versionInfo.patchUrl!,
|
||||
versionInfo.version,
|
||||
@@ -100,7 +147,6 @@ class UpdateDialog extends StatelessWidget {
|
||||
md5: versionInfo.patchMd5,
|
||||
);
|
||||
} else if (versionInfo.apkUrl != null) {
|
||||
// 整包更新
|
||||
context.read<UpdateCubit>().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<UpdateCubit>().cancelUpdate();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (versionInfo.apkUrl != null) {
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(versionInfo.apkUrl!);
|
||||
Navigator.of(context).pop();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('正在浏览器中打开下载链接...'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('立即下载'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String, dynamic> json) {
|
||||
// 兼容嵌套结构:如果存在 patch 对象,则从中提取 url 和 md5
|
||||
final patchData = json['patch'] as Map<String, dynamic>?;
|
||||
final fullApkData = json['fullApk'] as Map<String, dynamic>?;
|
||||
|
||||
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<void> clearPatchVersionInfo() async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
@@ -229,36 +236,54 @@ class VersionCheckService {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 保存补丁版本信息到文件(持久化,不会被清理缓存删除)
|
||||
/// 保存补丁版本信息到文件
|
||||
Future<void> _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<Map<String, dynamic>> _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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AuthState> {
|
||||
/// App 启动时检查本地缓存
|
||||
Future<void> appStarted() async {
|
||||
final logger = GetIt.I<ILoggerService>() 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<AuthState> {
|
||||
/// 当 HTTP 登录/注册成功后调用
|
||||
Future<void> 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<AuthState> {
|
||||
try {
|
||||
final devicesCubit = GetIt.I<DevicesCubit>();
|
||||
final deviceStatusBloc = GetIt.I<DeviceStatusBloc>();
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
|
||||
// 1. 清空设备列表和选中设备
|
||||
devicesCubit.emit(const DevicesState());
|
||||
@@ -105,6 +114,11 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
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<AuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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');
|
||||
|
||||
@@ -21,11 +21,11 @@ class LoginCubit extends Cubit<LoginState> {
|
||||
emit(LoginLoading());
|
||||
try {
|
||||
final result = await loginUseCase.call(LoginParams(username, password, sourceType));
|
||||
final user = sl<AppUserCubit>().state.user;
|
||||
final devicesCubit = sl<DevicesCubit>();
|
||||
if (user != null) {
|
||||
devicesCubit.fetchAllDevices(user.username);
|
||||
}
|
||||
// final user = sl<AppUserCubit>().state.user;
|
||||
// final devicesCubit = sl<DevicesCubit>();
|
||||
// if (user != null) {
|
||||
// devicesCubit.fetchAllDevices(user.username);
|
||||
// }
|
||||
result.fold((failure) => emit(LoginFailure(failure.message)), (user) {
|
||||
emit(LoginSuccess(user));
|
||||
authCubit.loginSuccess(user);
|
||||
|
||||
@@ -38,8 +38,11 @@ class _LoginPageState extends State<LoginPage> {
|
||||
appBar: AppBar(backgroundColor: Colors.white, elevation: 0),
|
||||
body: BlocListener<LoginCubit, LoginState>(
|
||||
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<LoginPage> {
|
||||
|
||||
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),
|
||||
|
||||
@@ -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<SiteCubit>().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<DroneStationBloc>()..add(DroneStationLoadData(selectedSite.id)),
|
||||
child: BlocBuilder<DroneStationBloc, DroneStationState>(
|
||||
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<DroneStationBloc>().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<DroneStationBloc>().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('暂无数据'));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DroneStationDetailPage> createState() => _DroneStationDetailPageState();
|
||||
}
|
||||
|
||||
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
_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<DroneStationBloc, DroneStationState>(
|
||||
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<CameraInfo> 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>(Color(0xFF00B42A)),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'78%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<HomeV2Event, HomeV2State> {
|
||||
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<HomeV2LoadData>(_onLoadData);
|
||||
on<HomeV2Refresh>(_onRefresh);
|
||||
on<HomeV2ToggleTrendType>(_onToggleTrendType);
|
||||
@@ -18,11 +25,55 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
|
||||
) 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<SiteEntity> 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<HomeV2Event, HomeV2State> {
|
||||
Emitter<HomeV2State> 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,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Object?> get props => [type];
|
||||
}
|
||||
|
||||
class HomeV2SelectSite extends HomeV2Event {
|
||||
final SiteEntity site;
|
||||
|
||||
const HomeV2SelectSite(this.site);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [site];
|
||||
}
|
||||
|
||||
@@ -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<SiteEntity> sites;
|
||||
final SiteEntity? selectedSite;
|
||||
|
||||
const HomeV2Loaded({
|
||||
required this.homeData,
|
||||
this.trendType = 'kW',
|
||||
this.sites = const [],
|
||||
this.selectedSite,
|
||||
});
|
||||
|
||||
HomeV2Loaded copyWith({
|
||||
HomeEntity? homeData,
|
||||
String? trendType,
|
||||
List<SiteEntity>? sites,
|
||||
SiteEntity? selectedSite,
|
||||
}) {
|
||||
return HomeV2Loaded(
|
||||
homeData: homeData ?? this.homeData,
|
||||
trendType: trendType ?? this.trendType,
|
||||
sites: sites ?? this.sites,
|
||||
selectedSite: selectedSite ?? this.selectedSite,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [homeData, trendType];
|
||||
List<Object?> get props => [homeData, trendType, sites, selectedSite];
|
||||
}
|
||||
|
||||
class HomeV2Error extends HomeV2State {
|
||||
|
||||
@@ -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<HomeV2Page> {
|
||||
late HomeV2Bloc _bloc;
|
||||
String _selectedPlant = '示例光伏电站';
|
||||
final List<String> _plantList = [
|
||||
'示例光伏电站',
|
||||
'一号光伏电站',
|
||||
'二号光伏电站',
|
||||
'三号光伏电站',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -82,15 +77,21 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
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<SiteState>(
|
||||
stream: sl<SiteCubit>().stream,
|
||||
builder: (context, snapshot) {
|
||||
final siteState = snapshot.data ?? sl<SiteCubit>().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<HomeV2Page> {
|
||||
}
|
||||
|
||||
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<HomeV2Page> {
|
||||
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<HomeV2Bloc, HomeV2State>(
|
||||
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<SiteCubit>().selectSite(site);
|
||||
Navigator.pop(context);
|
||||
|
||||
// 切换场站后,重新加载首页数据
|
||||
_bloc.add(const HomeV2LoadData());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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<MyCubit>();
|
||||
final appUserState = context.watch<AppUserCubit>().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;
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
216
lib/main.dart
216
lib/main.dart
@@ -41,6 +41,11 @@ void main() async {
|
||||
final logger = sl<ILoggerService>();
|
||||
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<UpdateCubit>().applyPatch(
|
||||
info.patchUrl!,
|
||||
info.version,
|
||||
info.versionCode,
|
||||
md5: info.patchMd5,
|
||||
);
|
||||
} else if (info.apkUrl != null) {
|
||||
// 🔥 执行整包更新
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
||||
}
|
||||
},
|
||||
child: const Text('立即更新'),
|
||||
),
|
||||
if (state.versionInfo.updateType == 'patch')
|
||||
// 🔥 差量更新:单个按钮
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final info = state.versionInfo;
|
||||
if (info.patchUrl != null) {
|
||||
context.read<UpdateCubit>().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<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
||||
}
|
||||
},
|
||||
child: const Text('手动下载'),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final info = state.versionInfo;
|
||||
if (info.apkUrl != null) {
|
||||
context.read<UpdateCubit>().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<UpdateCubit>().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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user