对接路径规划的列表(根据场站的下的)
优化无人机机场的监控视频的显示视频流畅度提升80% 添加首页tcp指示灯的机器状态信息展示功能。(暂不支持在此页面上设备的切换显示)
This commit is contained in:
@@ -98,7 +98,9 @@
|
||||
"value": "Value",
|
||||
"initialized": "Initialized",
|
||||
"no_data": "No Data",
|
||||
"voltage": "Voltage"
|
||||
"voltage": "Voltage",
|
||||
"tcp_reconnected": "TCP connection restored",
|
||||
"tcp_reconnect_failed": "TCP reconnection failed, please check network"
|
||||
},
|
||||
|
||||
"machine_details": {
|
||||
|
||||
@@ -98,7 +98,9 @@
|
||||
"value": "数值",
|
||||
"initialized": "已初始化",
|
||||
"no_data": "暂无数据",
|
||||
"voltage": "电压"
|
||||
"voltage": "电压",
|
||||
"tcp_reconnected": "TCP连接已恢复",
|
||||
"tcp_reconnect_failed": "TCP重连失败,请检查网络"
|
||||
},
|
||||
|
||||
"machine_details": {
|
||||
|
||||
1347
lib/components/device_status_modal.dart
Normal file
1347
lib/components/device_status_modal.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,13 @@ import 'package:maibu_satabot_v2/core/network/tcp/tcp_status_cubit.dart';
|
||||
class TcpStatusIndicator extends StatefulWidget {
|
||||
final double size;
|
||||
final bool showDisconnected; // 是否显示未连接状态
|
||||
final VoidCallback? onTap; // 点击回调
|
||||
|
||||
const TcpStatusIndicator({
|
||||
super.key,
|
||||
this.size = 12.0,
|
||||
this.showDisconnected = false, // 默认不显示未连接状态
|
||||
this.onTap, // 点击回调
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -27,18 +29,19 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tcpStatusCubit = GetIt.I<TcpStatusCubit>();
|
||||
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0.6, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller!, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: 0.6,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(parent: _controller!, curve: Curves.easeInOut));
|
||||
|
||||
_controller!.repeat(reverse: true);
|
||||
|
||||
|
||||
_tcpStatusCubit.stream.listen((state) {
|
||||
if (state.status != TcpConnectionStatus.disconnected) {
|
||||
_hasActivity = true;
|
||||
@@ -95,28 +98,32 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation!,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation!.value : 1.0;
|
||||
return Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: BorderRadius.circular(widget.size / 2),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation!,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation!.value : 1.0;
|
||||
return Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,4 +36,7 @@ class HttpApiConsts {
|
||||
|
||||
// 获取机器人列表
|
||||
static const String getRobotList = "$baseUrl/iot/device/getSiteList";
|
||||
|
||||
// 获取飞行任务列表
|
||||
static const String getFlightTask = "$baseUrl/iot/UAV/getFlightTask";
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import '../../features/devices/domain/usecases/device_work_hostrirty_usecase.dar
|
||||
import '../../features/devices/domain/usecases/generate_path_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/get_device_location_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/get_work_record_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/get_work_records_by_site_id_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/route_planning_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/save_work_record_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/select_work_record_usecase.dart';
|
||||
@@ -362,25 +363,26 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl()));
|
||||
sl.registerLazySingleton(
|
||||
() => DevicesCubit(
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(),
|
||||
sl(), // repository
|
||||
sl(), // GetUserDeviceUseCase
|
||||
sl(), // GetDeviceLocationUseCase
|
||||
sl(), // GetWorkRecordUseCase
|
||||
sl(), // GetWorkRecordsBySiteIdUseCase (NEW)
|
||||
sl(), // DeleteWorkRecordUseCase
|
||||
sl(), // UnbindDeviceUseCase
|
||||
sl(), // UpdateDevicenameUsecase
|
||||
sl(), // SelectWorkRecordUseCase
|
||||
sl(), // SaveWorkRecordUseCase
|
||||
sl(), // GeneratePathUseCase
|
||||
sl(), // RoutePlanningUseCase
|
||||
sl(), // BindDeviceUseCase
|
||||
sl(), // DeviceStatusBloc
|
||||
sl(), // TcpClient
|
||||
sl(), // PathPlanningService
|
||||
),
|
||||
);
|
||||
|
||||
// 🔥 DeviceStatusBloc 必须在 RemoteControlCubit 之前注册(单例)
|
||||
// DeviceStatusBloc 必须在 RemoteControlCubit 之前注册(单例)
|
||||
sl.registerLazySingleton(
|
||||
() => DeviceStatusBloc(
|
||||
sl<NetMessageDispatcher>(),
|
||||
@@ -417,6 +419,11 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton(() => GetWorkRecordUseCase(sl<PathRepository>()));
|
||||
sl.registerLazySingleton(() => DeleteWorkRecordUseCase(sl<PathRepository>()));
|
||||
|
||||
/// 根据场站ID获取工作记录(XML格式)
|
||||
sl.registerLazySingleton(
|
||||
() => GetWorkRecordsBySiteIdUseCase(sl<PathRepository>()),
|
||||
);
|
||||
|
||||
/// 6. 认证 (Auth)
|
||||
// --- 关键修改点 1: AuthCubit 必须在 GoRouter 之前注册,并传入参数 ---
|
||||
sl.registerLazySingleton(
|
||||
|
||||
237
lib/features/devices/data/models/work_record_entity.dart
Normal file
237
lib/features/devices/data/models/work_record_entity.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
/// 工作记录实体(用于XML解析)
|
||||
class WorkRecordEntity {
|
||||
final String? createBy;
|
||||
final String? createTime;
|
||||
final String? updateBy;
|
||||
final String? updateTime;
|
||||
final bool delFlag;
|
||||
final String? remark;
|
||||
final int orgId;
|
||||
final int siteId;
|
||||
final int userId;
|
||||
final int id;
|
||||
final String workName;
|
||||
final WorkRecordJsonData? jsonData;
|
||||
final String? imgUrl;
|
||||
|
||||
WorkRecordEntity({
|
||||
this.createBy,
|
||||
this.createTime,
|
||||
this.updateBy,
|
||||
this.updateTime,
|
||||
required this.delFlag,
|
||||
this.remark,
|
||||
required this.orgId,
|
||||
required this.siteId,
|
||||
required this.userId,
|
||||
required this.id,
|
||||
required this.workName,
|
||||
this.jsonData,
|
||||
this.imgUrl,
|
||||
});
|
||||
|
||||
factory WorkRecordEntity.fromXml(Map<String, dynamic> xmlData) {
|
||||
return WorkRecordEntity(
|
||||
createBy: xmlData['createBy'] as String?,
|
||||
createTime: xmlData['createTime'] as String?,
|
||||
updateBy: xmlData['updateBy'] as String?,
|
||||
updateTime: xmlData['updateTime'] as String?,
|
||||
delFlag: xmlData['delFlag'] == 'true',
|
||||
remark: xmlData['remark'] as String?,
|
||||
orgId: int.tryParse(xmlData['orgId']?.toString() ?? '0') ?? 0,
|
||||
siteId: int.tryParse(xmlData['siteId']?.toString() ?? '0') ?? 0,
|
||||
userId: int.tryParse(xmlData['userId']?.toString() ?? '0') ?? 0,
|
||||
id: int.tryParse(xmlData['id']?.toString() ?? '0') ?? 0,
|
||||
workName: xmlData['workName'] as String? ?? '',
|
||||
jsonData: xmlData['jsonData'] != null
|
||||
? WorkRecordJsonData.fromXml(
|
||||
xmlData['jsonData'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
imgUrl: xmlData['imgUrl'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
factory WorkRecordEntity.fromJson(Map<String, dynamic> json) {
|
||||
return WorkRecordEntity(
|
||||
createBy: json['createBy'] as String?,
|
||||
createTime: json['createTime'] as String?,
|
||||
updateBy: json['updateBy'] as String?,
|
||||
updateTime: json['updateTime'] as String?,
|
||||
delFlag: json['delFlag'] == true || json['delFlag'] == 'true',
|
||||
remark: json['remark'] as String?,
|
||||
orgId: _parseInt(json['orgId']),
|
||||
siteId: _parseInt(json['siteId']),
|
||||
userId: _parseInt(json['userId']),
|
||||
id: _parseInt(json['id']),
|
||||
workName: json['workName'] as String? ?? '',
|
||||
jsonData: json['jsonData'] != null
|
||||
? WorkRecordJsonData.fromJson(
|
||||
json['jsonData'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
imgUrl: json['imgUrl'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
static int _parseInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 工作记录JSON数据
|
||||
class WorkRecordJsonData {
|
||||
final String? name;
|
||||
final List<Map<String, double>>? path;
|
||||
final List<Map<String, double>>? outer;
|
||||
final String? img;
|
||||
final int? planModel;
|
||||
|
||||
WorkRecordJsonData({
|
||||
this.name,
|
||||
this.path,
|
||||
this.outer,
|
||||
this.img,
|
||||
this.planModel,
|
||||
});
|
||||
|
||||
factory WorkRecordJsonData.fromXml(Map<String, dynamic> xmlData) {
|
||||
return WorkRecordJsonData(
|
||||
name: xmlData['name'] as String?,
|
||||
path: _parsePathList(xmlData['path']),
|
||||
outer: _parseOuterList(xmlData['outer']),
|
||||
img: xmlData['img'] as String?,
|
||||
planModel: int.tryParse(xmlData['planModel']?.toString() ?? '0'),
|
||||
);
|
||||
}
|
||||
|
||||
factory WorkRecordJsonData.fromJson(Map<String, dynamic> json) {
|
||||
return WorkRecordJsonData(
|
||||
name: json['name'] as String?,
|
||||
path: _parsePathListJson(json['path']),
|
||||
outer: _parseOuterListJson(json['outer']),
|
||||
img: json['img'] as String?,
|
||||
planModel: (json['planModel'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
static List<Map<String, double>>? _parsePathList(dynamic pathData) {
|
||||
if (pathData == null) return null;
|
||||
|
||||
// path 可能是 List 或 Map
|
||||
if (pathData is List) {
|
||||
return pathData.map((item) {
|
||||
if (item is Map) {
|
||||
return {
|
||||
'lat': double.tryParse(item['lat']?.toString() ?? '0') ?? 0.0,
|
||||
'lng': double.tryParse(item['lng']?.toString() ?? '0') ?? 0.0,
|
||||
};
|
||||
}
|
||||
return <String, double>{};
|
||||
}).toList();
|
||||
} else if (pathData is Map) {
|
||||
// 如果是单个 Map,检查是否包含 path 子节点
|
||||
if (pathData.containsKey('path')) {
|
||||
final pathList = pathData['path'];
|
||||
if (pathList is List) {
|
||||
return pathList.map((item) {
|
||||
if (item is Map) {
|
||||
return {
|
||||
'lat': double.tryParse(item['lat']?.toString() ?? '0') ?? 0.0,
|
||||
'lng': double.tryParse(item['lng']?.toString() ?? '0') ?? 0.0,
|
||||
};
|
||||
}
|
||||
return <String, double>{};
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
// 直接是单个 path 节点
|
||||
return [
|
||||
{
|
||||
'lat': double.tryParse(pathData['lat']?.toString() ?? '0') ?? 0.0,
|
||||
'lng': double.tryParse(pathData['lng']?.toString() ?? '0') ?? 0.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<Map<String, double>>? _parseOuterList(dynamic outerData) {
|
||||
if (outerData == null) return null;
|
||||
|
||||
// outer 可能是 List 或 Map
|
||||
if (outerData is List) {
|
||||
return outerData.map((item) {
|
||||
if (item is Map) {
|
||||
return {
|
||||
'lat': double.tryParse(item['lat']?.toString() ?? '0') ?? 0.0,
|
||||
'lng': double.tryParse(item['lng']?.toString() ?? '0') ?? 0.0,
|
||||
};
|
||||
}
|
||||
return <String, double>{};
|
||||
}).toList();
|
||||
} else if (outerData is Map) {
|
||||
// 如果是单个 Map,检查是否包含 outer 子节点
|
||||
if (outerData.containsKey('outer')) {
|
||||
final outerList = outerData['outer'];
|
||||
if (outerList is List) {
|
||||
return outerList.map((item) {
|
||||
if (item is Map) {
|
||||
return {
|
||||
'lat': double.tryParse(item['lat']?.toString() ?? '0') ?? 0.0,
|
||||
'lng': double.tryParse(item['lng']?.toString() ?? '0') ?? 0.0,
|
||||
};
|
||||
}
|
||||
return <String, double>{};
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
// 直接是单个 outer 节点
|
||||
return [
|
||||
{
|
||||
'lat': double.tryParse(outerData['lat']?.toString() ?? '0') ?? 0.0,
|
||||
'lng': double.tryParse(outerData['lng']?.toString() ?? '0') ?? 0.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// JSON格式解析path
|
||||
static List<Map<String, double>>? _parsePathListJson(dynamic pathData) {
|
||||
if (pathData == null) return null;
|
||||
if (pathData is List) {
|
||||
return pathData.map((item) {
|
||||
if (item is Map) {
|
||||
return {
|
||||
'lat': (item['lat'] as num?)?.toDouble() ?? 0.0,
|
||||
'lng': (item['lng'] as num?)?.toDouble() ?? 0.0,
|
||||
};
|
||||
}
|
||||
return <String, double>{};
|
||||
}).toList();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// JSON格式解析outer
|
||||
static List<Map<String, double>>? _parseOuterListJson(dynamic outerData) {
|
||||
if (outerData == null) return null;
|
||||
if (outerData is List) {
|
||||
return outerData.map((item) {
|
||||
if (item is Map) {
|
||||
return {
|
||||
'lat': (item['lat'] as num?)?.toDouble() ?? 0.0,
|
||||
'lng': (item['lng'] as num?)?.toDouble() ?? 0.0,
|
||||
};
|
||||
}
|
||||
return <String, double>{};
|
||||
}).toList();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/storage/user_storage.dart';
|
||||
@@ -9,10 +10,12 @@ import '../../domain/repositories/path_repository.dart';
|
||||
import '../datasources/path_http_datasource.dart';
|
||||
import '../models/device_add_path_point_model.dart';
|
||||
import '../models/device_work_area_param_model.dart';
|
||||
import '../models/work_record_entity.dart';
|
||||
|
||||
class PathRepositoryImpl implements PathRepository {
|
||||
final PathHttpDatasource _datasource;
|
||||
PathRepositoryImpl({required PathHttpDatasource datasource}) : _datasource = datasource;
|
||||
PathRepositoryImpl({required PathHttpDatasource datasource})
|
||||
: _datasource = datasource;
|
||||
// 生成路径
|
||||
@override
|
||||
Future<List<DeviceAddPathPointModel>> generatePath({
|
||||
@@ -38,10 +41,18 @@ class PathRepositoryImpl implements PathRepository {
|
||||
|
||||
// 保存工作记录
|
||||
@override
|
||||
Future<Map<String, dynamic>> saveWorkRecord({required String workName, required String userId, required String jsonData}) async {
|
||||
Future<Map<String, dynamic>> saveWorkRecord({
|
||||
required String workName,
|
||||
required String userId,
|
||||
required String jsonData,
|
||||
}) async {
|
||||
final url = Uri.parse('https://serviceri.satabot.com/iot/workRecord/add');
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
final body = jsonEncode({'workName': workName, 'userId': userId, 'jsonData': jsonData});
|
||||
final body = jsonEncode({
|
||||
'workName': workName,
|
||||
'userId': userId,
|
||||
'jsonData': jsonData,
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
@@ -57,7 +68,9 @@ class PathRepositoryImpl implements PathRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getWorkRecord({required String userId}) async {
|
||||
Future<List<Map<String, dynamic>>> getWorkRecord({
|
||||
required String userId,
|
||||
}) async {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final url = Uri.parse(
|
||||
'https://serviceri.satabot.com/iot/workRecord/selectByUserId',
|
||||
@@ -73,7 +86,9 @@ class PathRepositoryImpl implements PathRepository {
|
||||
throw Exception('API error: ${data['msg'] ?? 'Unknown'}');
|
||||
}
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}: ${response.reasonPhrase}');
|
||||
throw Exception(
|
||||
'HTTP ${response.statusCode}: ${response.reasonPhrase}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Network error in getWorkRecord: $e');
|
||||
@@ -81,11 +96,16 @@ class PathRepositoryImpl implements PathRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> deleteWorkRecord({required String workName}) async {
|
||||
Future<Map<String, dynamic>> deleteWorkRecord({
|
||||
required String workName,
|
||||
}) async {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final url = Uri.parse(
|
||||
'https://serviceri.satabot.com/iot/workRecord/deleteByWorkName',
|
||||
).replace(queryParameters: {'workName': workName, '_t': timestamp.toString()});
|
||||
final url =
|
||||
Uri.parse(
|
||||
'https://serviceri.satabot.com/iot/workRecord/deleteByWorkName',
|
||||
).replace(
|
||||
queryParameters: {'workName': workName, '_t': timestamp.toString()},
|
||||
);
|
||||
|
||||
try {
|
||||
final response = await http.get(url);
|
||||
@@ -94,7 +114,9 @@ class PathRepositoryImpl implements PathRepository {
|
||||
if (response.statusCode == 200 && data['code'] == 200) {
|
||||
return data; // 返回 {"code": 200, "msg": "删除成功"}
|
||||
} else {
|
||||
throw Exception('Delete failed: ${data['msg'] ?? response.reasonPhrase}');
|
||||
throw Exception(
|
||||
'Delete failed: ${data['msg'] ?? response.reasonPhrase}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Network error in deleteWorkRecord: $e');
|
||||
@@ -103,11 +125,16 @@ class PathRepositoryImpl implements PathRepository {
|
||||
|
||||
/// 选择工作记录
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> selectWorkRecordByName({required String workName}) async {
|
||||
Future<List<Map<String, dynamic>>> selectWorkRecordByName({
|
||||
required String workName,
|
||||
}) async {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final url = Uri.parse(
|
||||
'https://serviceri.satabot.com/iot/workRecord/selectByWorkName',
|
||||
).replace(queryParameters: {'workName': workName, '_t': timestamp.toString()});
|
||||
final url =
|
||||
Uri.parse(
|
||||
'https://serviceri.satabot.com/iot/workRecord/selectByWorkName',
|
||||
).replace(
|
||||
queryParameters: {'workName': workName, '_t': timestamp.toString()},
|
||||
);
|
||||
|
||||
try {
|
||||
final response = await http.get(url);
|
||||
@@ -131,7 +158,9 @@ class PathRepositoryImpl implements PathRepository {
|
||||
} else if (rawData is Map) {
|
||||
records = [Map<String, dynamic>.from(rawData)];
|
||||
} else {
|
||||
throw Exception('Unexpected data type for "data": ${rawData.runtimeType}');
|
||||
throw Exception(
|
||||
'Unexpected data type for "data": ${rawData.runtimeType}',
|
||||
);
|
||||
}
|
||||
|
||||
return records;
|
||||
@@ -139,10 +168,228 @@ class PathRepositoryImpl implements PathRepository {
|
||||
throw Exception('API error: ${data['msg'] ?? 'Unknown'}');
|
||||
}
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}: ${response.reasonPhrase}');
|
||||
throw Exception(
|
||||
'HTTP ${response.statusCode}: ${response.reasonPhrase}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Network error in selectWorkRecordByName: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据场站ID查询工作记录列表(XML格式)
|
||||
@override
|
||||
Future<List<WorkRecordEntity>> getWorkRecordsBySiteId({
|
||||
required int siteId,
|
||||
}) async {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final url =
|
||||
Uri.parse(
|
||||
'http://1.95.137.212:59015/iot/workRecord/selectBySiteId',
|
||||
).replace(
|
||||
queryParameters: {
|
||||
'siteId': siteId.toString(),
|
||||
'_t': timestamp.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {'Accept': 'application/xml, text/xml, */*'},
|
||||
);
|
||||
print('[XML接口] 响应状态码: ${response.statusCode}');
|
||||
print('[XML接口] 响应内容长度: ${response.body.length}');
|
||||
print('[XML接口] Content-Type: ${response.headers['content-type']}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// 打印前200字符确认格式
|
||||
final preview = response.body.length > 200
|
||||
? response.body.substring(0, 200)
|
||||
: response.body;
|
||||
print('[XML接口] 响应开头: $preview');
|
||||
|
||||
// 判断是JSON还是XML格式
|
||||
final trimmed = response.body.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
print('[XML接口] 检测到JSON格式,使用JSON解析');
|
||||
return _parseJsonResponse(response.body);
|
||||
} else {
|
||||
print('[XML接口] 检测到XML格式,使用XML解析');
|
||||
return _parseXmlResponse(response.body);
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
'HTTP ${response.statusCode}: ${response.reasonPhrase}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('[XML接口] 错误: $e');
|
||||
throw Exception('Network error in getWorkRecordsBySiteId: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析JSON格式响应
|
||||
Future<List<WorkRecordEntity>> _parseJsonResponse(String body) async {
|
||||
final data = jsonDecode(body) as Map<String, dynamic>;
|
||||
|
||||
if (data['code'] != 200) {
|
||||
throw Exception('API error: ${data['msg'] ?? 'Unknown'}');
|
||||
}
|
||||
|
||||
final recordsData = data['data'];
|
||||
if (recordsData == null) return [];
|
||||
|
||||
final List<WorkRecordEntity> records = [];
|
||||
|
||||
if (recordsData is List) {
|
||||
for (final item in recordsData) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
records.add(WorkRecordEntity.fromJson(item));
|
||||
}
|
||||
}
|
||||
} else if (recordsData is Map<String, dynamic>) {
|
||||
records.add(WorkRecordEntity.fromJson(recordsData));
|
||||
}
|
||||
|
||||
print('[XML接口] 最终解析记录数: ${records.length}');
|
||||
return records;
|
||||
}
|
||||
|
||||
/// 解析XML格式响应
|
||||
Future<List<WorkRecordEntity>> _parseXmlResponse(String body) async {
|
||||
// 用更宽松的正则检查响应码(支持命名空间前缀)
|
||||
final codeMatch = RegExp(
|
||||
r'<\w*:?code[^>]*>(\d+)</\w*:?code>',
|
||||
).firstMatch(body);
|
||||
final code = codeMatch?.group(1);
|
||||
print('[XML接口] code: $code');
|
||||
|
||||
if (code != '200') {
|
||||
final msgMatch = RegExp(
|
||||
r'<\w*:?msg[^>]*>(.*?)</\w*:?msg>',
|
||||
).firstMatch(body);
|
||||
throw Exception('API error: ${msgMatch?.group(1) ?? 'Unknown'}');
|
||||
}
|
||||
|
||||
// 使用正则表达式提取所有<data>...</data>节点
|
||||
// 使用非贪婪匹配,确保每个data节点独立提取
|
||||
final dataRegex = RegExp(r'<data>([\s\S]*?)</data>');
|
||||
final dataMatches = dataRegex.allMatches(body);
|
||||
print('[XML接口] 找到data节点数量: ${dataMatches.length}');
|
||||
|
||||
// 解析所有工作记录
|
||||
final List<WorkRecordEntity> records = [];
|
||||
|
||||
for (int i = 0; i < dataMatches.length; i++) {
|
||||
final match = dataMatches.elementAt(i);
|
||||
final dataContent = match.group(1)!; // 获取<data>和</data>之间的内容
|
||||
|
||||
try {
|
||||
// 将提取的内容包装成完整XML进行解析
|
||||
final wrappedXml = '<root>$dataContent</root>';
|
||||
final document = XmlDocument.parse(wrappedXml);
|
||||
final recordElement = document.rootElement;
|
||||
|
||||
final recordData = _parseXmlRecord(recordElement);
|
||||
print(
|
||||
'[XML接口] data[$i] 解析结果: workName=${recordData['workName']}, id=${recordData['id']}, imgUrl=${recordData['imgUrl']}',
|
||||
);
|
||||
|
||||
records.add(WorkRecordEntity.fromXml(recordData));
|
||||
} catch (e) {
|
||||
print('[XML接口] 解析单个data节点失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
print('[XML接口] 最终解析记录数: ${records.length}');
|
||||
return records;
|
||||
}
|
||||
|
||||
/// 解析XML工作记录节点
|
||||
Map<String, dynamic> _parseXmlRecord(XmlElement recordElement) {
|
||||
final Map<String, dynamic> result = {};
|
||||
|
||||
print('[XML解析] 开始解析节点,子元素数量: ${recordElement.childElements.length}');
|
||||
|
||||
for (final child in recordElement.childElements) {
|
||||
final tagName = child.name.local;
|
||||
final innerText = child.innerText.trim();
|
||||
|
||||
print(
|
||||
'[XML解析] 标签: $tagName, 值: ${innerText.length > 50 ? innerText.substring(0, 50) + '...' : innerText}',
|
||||
);
|
||||
|
||||
// 特殊处理jsonData节点(包含嵌套结构)
|
||||
if (tagName == 'jsonData') {
|
||||
result['jsonData'] = _parseJsonDataNode(child);
|
||||
} else {
|
||||
// 普通节点直接取值
|
||||
result[tagName] = innerText;
|
||||
}
|
||||
}
|
||||
|
||||
print('[XML解析] 解析完成,结果keys: ${result.keys.toList()}');
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 解析jsonData节点
|
||||
Map<String, dynamic> _parseJsonDataNode(XmlElement jsonDataElement) {
|
||||
final Map<String, dynamic> result = {};
|
||||
|
||||
for (final child in jsonDataElement.childElements) {
|
||||
final tagName = child.name.local;
|
||||
|
||||
if (tagName == 'path' || tagName == 'outer') {
|
||||
// path和outer可能包含嵌套的path/outer节点
|
||||
result[tagName] = _parseCoordinateList(child, tagName);
|
||||
} else {
|
||||
// 普通字段(name, img, planModel等)
|
||||
result[tagName] = child.innerText.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 解析坐标列表(path或outer)
|
||||
List<Map<String, double>> _parseCoordinateList(
|
||||
XmlElement element,
|
||||
String tagName,
|
||||
) {
|
||||
final List<Map<String, double>> coordinates = [];
|
||||
|
||||
// 检查是否有嵌套的同名节点
|
||||
final nestedElements = element.childElements
|
||||
.where((e) => e.name.local == tagName)
|
||||
.toList();
|
||||
|
||||
if (nestedElements.isNotEmpty) {
|
||||
// 有嵌套结构:outer > outer > {lat, lng}
|
||||
for (final nestedElement in nestedElements) {
|
||||
final latElement = nestedElement.getElement('lat');
|
||||
final lngElement = nestedElement.getElement('lng');
|
||||
|
||||
if (latElement != null && lngElement != null) {
|
||||
coordinates.add({
|
||||
'lat': double.tryParse(latElement.innerText.trim()) ?? 0.0,
|
||||
'lng': double.tryParse(lngElement.innerText.trim()) ?? 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接包含lat/lng节点
|
||||
final latElement = element.getElement('lat');
|
||||
final lngElement = element.getElement('lng');
|
||||
|
||||
if (latElement != null && lngElement != null) {
|
||||
coordinates.add({
|
||||
'lat': double.tryParse(latElement.innerText.trim()) ?? 0.0,
|
||||
'lng': double.tryParse(lngElement.innerText.trim()) ?? 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '../../data/models/device_add_path_point_model.dart';
|
||||
import '../../data/models/device_work_area_param_model.dart';
|
||||
import '../../data/models/work_record_entity.dart';
|
||||
|
||||
abstract class PathRepository {
|
||||
// Generate path(打点生成路径规划)
|
||||
@@ -20,6 +21,9 @@ abstract class PathRepository {
|
||||
// Delete work record (删除)
|
||||
Future<Map<String, dynamic>> deleteWorkRecord({required String workName});
|
||||
|
||||
/// 根据作业名查询路径记录(用于“选择一个路径”)
|
||||
/// 根据作业名查询路径记录(用于"选择一个路径")
|
||||
Future<List<Map<String, dynamic>>> selectWorkRecordByName({required String workName});
|
||||
|
||||
/// 根据场站ID查询工作记录列表(XML格式)
|
||||
Future<List<WorkRecordEntity>> getWorkRecordsBySiteId({required int siteId});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../data/models/work_record_entity.dart';
|
||||
import '../../domain/errors/device_failure.dart';
|
||||
import '../../domain/repositories/path_repository.dart';
|
||||
|
||||
class GetWorkRecordsBySiteIdUseCase {
|
||||
final PathRepository repository;
|
||||
|
||||
GetWorkRecordsBySiteIdUseCase(this.repository);
|
||||
|
||||
Future<Either<DeviceFailure, List<WorkRecordEntity>>> call(int siteId) async {
|
||||
try {
|
||||
final result = await repository.getWorkRecordsBySiteId(siteId: siteId);
|
||||
return Right(result);
|
||||
} catch (e) {
|
||||
return Left(DeviceFailure.networkError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/generate_path_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_user_device_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/models/work_record_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/save_work_record_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/unbind_device_usecase.dart';
|
||||
@@ -21,6 +23,7 @@ import '../../domain/usecases/bind_device_usecase.dart';
|
||||
import '../../domain/usecases/delete_work_record_usecase.dart';
|
||||
import '../../domain/usecases/get_device_location_usecase.dart';
|
||||
import '../../domain/usecases/get_work_record_usecase.dart';
|
||||
import '../../domain/usecases/get_work_records_by_site_id_usecase.dart';
|
||||
import '../../domain/usecases/route_planning_usecase.dart';
|
||||
import '../../services/path_planning_service.dart';
|
||||
import 'device_status_bloc.dart';
|
||||
@@ -32,6 +35,7 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
final GetDeviceLocationUseCase _getDeviceLocationUseCase;
|
||||
final DeviceRepository repository;
|
||||
final GetWorkRecordUseCase _getWorkRecordUseCase;
|
||||
final GetWorkRecordsBySiteIdUseCase _getWorkRecordsBySiteIdUseCase;
|
||||
final DeleteWorkRecordUseCase _deleteWorkRecordUseCase;
|
||||
final BindDeviceUseCase _bindDeviceUseCase;
|
||||
final UnbindDeviceUseCase _unbindDeviceUseCase;
|
||||
@@ -52,6 +56,7 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
this._getUserDeviceUseCase,
|
||||
this._getDeviceLocationUseCase,
|
||||
this._getWorkRecordUseCase,
|
||||
this._getWorkRecordsBySiteIdUseCase,
|
||||
this._deleteWorkRecordUseCase,
|
||||
this._unbindDeviceUseCase,
|
||||
this._updateDevicename,
|
||||
@@ -66,7 +71,13 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
) : super(const DevicesState());
|
||||
|
||||
Future<void> unbindDevice(String deviceId, String deviceName) async {
|
||||
emit(state.copyWith(isLoading: true, errorMessage: '', operationType: DeviceOperationType.unbind));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: true,
|
||||
errorMessage: '',
|
||||
operationType: DeviceOperationType.unbind,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final params = UnbindDeviceParams(deviceId, deviceName);
|
||||
@@ -92,55 +103,94 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
devices: updatedDevices,
|
||||
selectedDevice: state.selectedDevice?.deviceName == deviceId ? null : state.selectedDevice,
|
||||
selectedDevice: state.selectedDevice?.deviceName == deviceId
|
||||
? null
|
||||
: state.selectedDevice,
|
||||
errorMessage: '',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: '解绑失败:状态码 $successCode', operationType: DeviceOperationType.none));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '解绑失败:状态码 $successCode',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: '解绑异常:${e.toString()}', operationType: DeviceOperationType.none));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '解绑异常:${e.toString()}',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateDeviceName(String deviceId, String deviceName) async {
|
||||
emit(state.copyWith(isLoading: true, errorMessage: '', operationType: DeviceOperationType.updateName));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: true,
|
||||
errorMessage: '',
|
||||
operationType: DeviceOperationType.updateName,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final params = UpdateDevicenameParams(deviceId, deviceName);
|
||||
final result = await _updateDevicename.call(params);
|
||||
|
||||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '更新设备名称失败', operationType: DeviceOperationType.none)), (
|
||||
successCode,
|
||||
) {
|
||||
// 核心修复:int 转 bool 条件判断
|
||||
//print('更新设备名称结果代码: $successCode'); // 调试输出结果代码
|
||||
_logger.logWithLevel('更新设备名称结果代码: $successCode');
|
||||
final isSuccess = successCode == 1; // 显式转为 bool
|
||||
if (isSuccess) {
|
||||
//final updatedDevices = state.devices?.map((device) {
|
||||
// return device.deviceName == deviceId ? device.copyWith(deviceName: deviceName) : device;
|
||||
//}).toList();
|
||||
result.fold(
|
||||
(failure) => emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message ?? '更新设备名称失败',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
),
|
||||
(successCode) {
|
||||
// 核心修复:int 转 bool 条件判断
|
||||
//print('更新设备名称结果代码: $successCode'); // 调试输出结果代码
|
||||
_logger.logWithLevel('更新设备名称结果代码: $successCode');
|
||||
final isSuccess = successCode == 1; // 显式转为 bool
|
||||
if (isSuccess) {
|
||||
//final updatedDevices = state.devices?.map((device) {
|
||||
// return device.deviceName == deviceId ? device.copyWith(deviceName: deviceName) : device;
|
||||
//}).toList();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
//devices: updatedDevices,
|
||||
//selectedDevice: state.selectedDevice?.deviceName == deviceId ? state.selectedDevice?.copyWith(deviceName: deviceName) : state.selectedDevice,
|
||||
errorMessage: '',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: '更新设备名称失败:状态码 $successCode', operationType: DeviceOperationType.none));
|
||||
}
|
||||
});
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
//devices: updatedDevices,
|
||||
//selectedDevice: state.selectedDevice?.deviceName == deviceId ? state.selectedDevice?.copyWith(deviceName: deviceName) : state.selectedDevice,
|
||||
errorMessage: '',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '更新设备名称失败:状态码 $successCode',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: '更新设备名称异常:${e.toString()}', operationType: DeviceOperationType.none));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '更新设备名称异常:${e.toString()}',
|
||||
operationType: DeviceOperationType.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,32 +204,39 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
final String? oldSelectedDeviceName = state.selectedDevice?.deviceName;
|
||||
|
||||
// 网络请求获取新列表
|
||||
var resultEither = await _getUserDeviceUseCase.call(GetUserDeviceParams(username));
|
||||
var resultEither = await _getUserDeviceUseCase.call(
|
||||
GetUserDeviceParams(username),
|
||||
);
|
||||
|
||||
resultEither.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)), (deviceList) {
|
||||
// 🔥 关键步骤2:匹配新列表中对应的旧选中设备
|
||||
DeviceEntity? newSelectedDevice;
|
||||
if (oldSelectedDeviceName != null && deviceList.isNotEmpty) {
|
||||
// 在新列表中查找和旧选中设备名称一致的设备
|
||||
newSelectedDevice = deviceList.firstWhere(
|
||||
(device) => device.deviceName == oldSelectedDeviceName,
|
||||
// 如果找不到(如设备已解绑),返回 null
|
||||
orElse: () => deviceList.first, // 兜底:选中第一个
|
||||
resultEither.fold(
|
||||
(failure) => emit(
|
||||
state.copyWith(isLoading: false, errorMessage: failure.message),
|
||||
),
|
||||
(deviceList) {
|
||||
// 🔥 关键步骤2:匹配新列表中对应的旧选中设备
|
||||
DeviceEntity? newSelectedDevice;
|
||||
if (oldSelectedDeviceName != null && deviceList.isNotEmpty) {
|
||||
// 在新列表中查找和旧选中设备名称一致的设备
|
||||
newSelectedDevice = deviceList.firstWhere(
|
||||
(device) => device.deviceName == oldSelectedDeviceName,
|
||||
// 如果找不到(如设备已解绑),返回 null
|
||||
orElse: () => deviceList.first, // 兜底:选中第一个
|
||||
);
|
||||
} else {
|
||||
// 无旧选中设备,默认选中第一个
|
||||
newSelectedDevice = deviceList.isNotEmpty ? deviceList.first : null;
|
||||
}
|
||||
|
||||
// 🔥 关键步骤3:更新状态,使用匹配后的选中设备
|
||||
emit(
|
||||
state.copyWith(
|
||||
devices: deviceList,
|
||||
selectedDevice: newSelectedDevice, // 保留旧选中设备
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 无旧选中设备,默认选中第一个
|
||||
newSelectedDevice = deviceList.isNotEmpty ? deviceList.first : null;
|
||||
}
|
||||
|
||||
// 🔥 关键步骤3:更新状态,使用匹配后的选中设备
|
||||
emit(
|
||||
state.copyWith(
|
||||
devices: deviceList,
|
||||
selectedDevice: newSelectedDevice, // 保留旧选中设备
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
||||
}
|
||||
@@ -197,7 +254,10 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
}).toList();
|
||||
|
||||
// 如果更新的是当前选中的设备,也要同步更新 selectedDevice
|
||||
final newSelected = state.selectedDevice?.deviceName == updatedDevice.deviceName ? updatedDevice : state.selectedDevice;
|
||||
final newSelected =
|
||||
state.selectedDevice?.deviceName == updatedDevice.deviceName
|
||||
? updatedDevice
|
||||
: state.selectedDevice;
|
||||
|
||||
emit(state.copyWith(devices: newList, selectedDevice: newSelected));
|
||||
}
|
||||
@@ -233,7 +293,11 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
// connect 方法内部会自动调用 _sendAuthPacket -> 获取设备列表 -> 自动订阅当前选中的设备
|
||||
// debugPrint('🔌 开始重新新连接 TCP,将自动订阅新设备:${device.deviceName}');
|
||||
_logger.logWithLevel('🔌 启动重新新连接 TCP,将自动订阅新设备:${device.deviceName}');
|
||||
await _tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: device.deviceName);
|
||||
await _tcpClient.connectBySwitch(
|
||||
host: TCPConsts.TCP_IP,
|
||||
port: TCPConsts.TCP_PORT,
|
||||
deviceName: device.deviceName,
|
||||
);
|
||||
// 🔥 关键修复 3:重置设备状态 Bloc,清除旧设备图表数据
|
||||
_deviceStatusBloc.add(DeviceStatusReset());
|
||||
|
||||
@@ -257,7 +321,12 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
result.fold(
|
||||
// 失败处理
|
||||
(failure) {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '绑定设备失败'));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message ?? '绑定设备失败',
|
||||
),
|
||||
);
|
||||
// 抛出异常,携带后端返回的错误消息(如“设备不存在”)
|
||||
throw Exception(failure.message ?? '绑定设备失败');
|
||||
},
|
||||
@@ -268,7 +337,12 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
// 绑定成功后刷新设备列表
|
||||
//fetchAllDevices(state.?.username ?? '');
|
||||
} else {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: '绑定失败:状态码 $successCode'));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '绑定失败:状态码 $successCode',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -287,8 +361,15 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _getDeviceLocationUseCase.call(device.deviceName);
|
||||
result.fold(
|
||||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(location) => emit(state.copyWith(isLoading: false, deviceLatitude: location.latitude, deviceLongitude: location.longitude)),
|
||||
(failure) =>
|
||||
emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(location) => emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
deviceLatitude: location.latitude,
|
||||
deviceLongitude: location.longitude,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,18 +378,71 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _getWorkRecordUseCase(userId);
|
||||
result.fold(
|
||||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(failure) =>
|
||||
emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(records) => emit(state.copyWith(isLoading: false, workRecords: records)),
|
||||
);
|
||||
}
|
||||
|
||||
/// 根据场站ID获取工作记录(XML格式接口)
|
||||
Future<void> loadWorkRecordsBySiteId(int siteId) async {
|
||||
print('🔍 [DevicesCubit] 开始加载场站ID=$siteId的工作记录');
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _getWorkRecordsBySiteIdUseCase(siteId);
|
||||
result.fold(
|
||||
(failure) {
|
||||
print('❌ [DevicesCubit] 加载失败: ${failure.message}');
|
||||
emit(state.copyWith(isLoading: false, errorMessage: failure.message));
|
||||
},
|
||||
(records) {
|
||||
print('🔍 [DevicesCubit] 加载成功,记录数: ${records.length}');
|
||||
// 将 WorkRecordEntity 转换为 Map<String, dynamic> 以兼容现有UI
|
||||
final mappedRecords = records.map((record) {
|
||||
return <String, dynamic>{
|
||||
'id': record.id.toString(),
|
||||
'workName': record.workName,
|
||||
'imgUrl': record.imgUrl ?? '',
|
||||
'jsonData': record.jsonData != null
|
||||
? _workRecordJsonDataToJson(record.jsonData!)
|
||||
: null,
|
||||
};
|
||||
}).toList();
|
||||
print('🔍 [DevicesCubit] 转换后的数据: $mappedRecords');
|
||||
emit(state.copyWith(isLoading: false, workRecords: mappedRecords));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 将 WorkRecordJsonData 转换为 JSON 字符串
|
||||
String _workRecordJsonDataToJson(dynamic jsonData) {
|
||||
// 将 jsonData 对象序列化为 JSON 字符串供UI使用
|
||||
if (jsonData is WorkRecordJsonData) {
|
||||
return jsonEncode({
|
||||
'name': jsonData.name,
|
||||
'path': jsonData.path,
|
||||
'outer': jsonData.outer,
|
||||
'img': jsonData.img,
|
||||
'planModel': jsonData.planModel,
|
||||
});
|
||||
}
|
||||
return jsonData.toString();
|
||||
}
|
||||
|
||||
/// 删除工作记录
|
||||
Future<void> deleteWorkRecord(String workName) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _deleteWorkRecordUseCase(workName);
|
||||
result.fold(
|
||||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(_) => emit(state.copyWith(isLoading: false, workRecords: state.workRecords?.where((record) => record != workName).toList())),
|
||||
(failure) =>
|
||||
emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(_) => emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
workRecords: state.workRecords
|
||||
?.where((record) => record != workName)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,7 +452,9 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
try {
|
||||
final result = await _selectWorkRecordUseCase.call(workName);
|
||||
result.fold(
|
||||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(failure) => emit(
|
||||
state.copyWith(isLoading: false, errorMessage: failure.message),
|
||||
),
|
||||
(data) => emit(state.copyWith(isLoading: false, pathData: data)),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -327,10 +463,22 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
}
|
||||
|
||||
/// 保存路径数据
|
||||
Future<void> saveWorkRecord(String workName, String userId, String jsonData) async {
|
||||
Future<void> saveWorkRecord(
|
||||
String workName,
|
||||
String userId,
|
||||
String jsonData,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _saveWorkRecordUseCase.call(workName: workName, userId: userId, jsonData: jsonData);
|
||||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)), (data) => emit(state.copyWith(isLoading: false)));
|
||||
final result = await _saveWorkRecordUseCase.call(
|
||||
workName: workName,
|
||||
userId: userId,
|
||||
jsonData: jsonData,
|
||||
);
|
||||
result.fold(
|
||||
(failure) =>
|
||||
emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||||
(data) => emit(state.copyWith(isLoading: false)),
|
||||
);
|
||||
}
|
||||
|
||||
/// generatePath
|
||||
@@ -346,13 +494,24 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
// debugPrint('第${index + 1}组holes:$pointsStr');
|
||||
//});
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _generatePathUseCase.execute(reference: reference, heading: heading, outer: outer, holes: holes, workType: workType);
|
||||
final result = await _generatePathUseCase.execute(
|
||||
reference: reference,
|
||||
heading: heading,
|
||||
outer: outer,
|
||||
holes: holes,
|
||||
workType: workType,
|
||||
);
|
||||
|
||||
result.fold((failure) => emit(state.copyWith(errorMessage: failure.message)), (pathData) => emit(state.copyWith(generatedPath: pathData)));
|
||||
result.fold(
|
||||
(failure) => emit(state.copyWith(errorMessage: failure.message)),
|
||||
(pathData) => emit(state.copyWith(generatedPath: pathData)),
|
||||
);
|
||||
}
|
||||
|
||||
// 开始路径规划
|
||||
Future<void> startRoutePlanning(Queue<DeviceAddPathPointModel> locationQueue) async {
|
||||
Future<void> startRoutePlanning(
|
||||
Queue<DeviceAddPathPointModel> locationQueue,
|
||||
) async {
|
||||
/// print("cubit层开始路径规划");
|
||||
_logger.logWithLevel('开始路径规划');
|
||||
// 清空全局 Service 中的队列
|
||||
@@ -373,7 +532,12 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
_logger.logWithLevel('从 Service 获取的队列长度:${queue.length}');
|
||||
final result = await _routePlanningUseCase.startRoutePlanning(queue);
|
||||
result.fold(
|
||||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '路径规划启动失败')),
|
||||
(failure) => emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message ?? '路径规划启动失败',
|
||||
),
|
||||
),
|
||||
(_) => emit(state.copyWith(isLoading: false, errorMessage: '')),
|
||||
);
|
||||
}
|
||||
@@ -382,14 +546,30 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
Future<void> pauseRoutePlanning() async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _routePlanningUseCase.pauseRPWork();
|
||||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '暂停失败')), (_) => emit(state.copyWith(isLoading: false)));
|
||||
result.fold(
|
||||
(failure) => emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message ?? '暂停失败',
|
||||
),
|
||||
),
|
||||
(_) => emit(state.copyWith(isLoading: false)),
|
||||
);
|
||||
}
|
||||
|
||||
// 恢复
|
||||
Future<void> resumeRoutePlanning() async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _routePlanningUseCase.resumeRPWork();
|
||||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '恢复失败')), (_) => emit(state.copyWith(isLoading: false)));
|
||||
result.fold(
|
||||
(failure) => emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message ?? '恢复失败',
|
||||
),
|
||||
),
|
||||
(_) => emit(state.copyWith(isLoading: false)),
|
||||
);
|
||||
}
|
||||
|
||||
// 停止
|
||||
@@ -399,7 +579,15 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
|
||||
// 🔥 重置 PathPlanningService 中的全局的队列
|
||||
_pathPlanningService.clear();
|
||||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '停止失败')), (_) => emit(state.copyWith(isLoading: false)));
|
||||
result.fold(
|
||||
(failure) => emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: failure.message ?? '停止失败',
|
||||
),
|
||||
),
|
||||
(_) => emit(state.copyWith(isLoading: false)),
|
||||
);
|
||||
}
|
||||
|
||||
void updateAppState(AppState appState) {
|
||||
@@ -419,7 +607,9 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
void setArrivedLocation(double latitude, double longitude) {
|
||||
emit(state.copyWith(arriLatitude: latitude, arriLongitude: longitude));
|
||||
// print('✅ [DevicesCubit] 监听更新已完成到达位置:Lat=$latitude, Lng=$longitude');
|
||||
_logger.logWithLevel('✅ [DevicesCubit] 监听更新已完成到达位置:Lat=$latitude, Lng=$longitude');
|
||||
_logger.logWithLevel(
|
||||
'✅ [DevicesCubit] 监听更新已完成到达位置:Lat=$latitude, Lng=$longitude',
|
||||
);
|
||||
}
|
||||
|
||||
//获取已到达的点的经纬度
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'dart:math' as math;
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart';
|
||||
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||
@@ -15,6 +16,8 @@ import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/network/net_message_dispatcher.dart';
|
||||
import '../../../../core/network/protocol_decoder.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../auth/presentation/bloc/auth_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/device_status_bloc.dart';
|
||||
import '../../../devices/presentation/bloc/device_status_event.dart';
|
||||
@@ -81,6 +84,9 @@ class _RunningStatusPageState extends State<RunningStatusPage> with WidgetsBindi
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
debugPrint('🔄 [RunningStatusPage] 应用恢复,强制刷新UI');
|
||||
_forceRefresh();
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
debugPrint('⏸️ [RunningStatusPage] 应用进入后台,暂停超时计时器');
|
||||
_dataTimeoutTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1056,11 +1062,53 @@ class _RunningStatusPageState extends State<RunningStatusPage> with WidgetsBindi
|
||||
void _forceRefresh() {
|
||||
if (mounted) {
|
||||
debugPrint('🔄 [RunningStatusPage] 执行强制刷新');
|
||||
setState(() {
|
||||
// 触发 UI 重建
|
||||
});
|
||||
// 重置超时计时器
|
||||
_startDataTimeoutTimer();
|
||||
|
||||
final tcpClient = sl<TcpClient>();
|
||||
if (!tcpClient.isConnected) {
|
||||
debugPrint('⚠️ [RunningStatusPage] TCP未连接,尝试重连');
|
||||
_reconnectTcp();
|
||||
} else {
|
||||
setState(() {
|
||||
_isDataTimeout = false;
|
||||
});
|
||||
_startDataTimeoutTimer();
|
||||
|
||||
debugPrint('✅ [RunningStatusPage] TCP已连接,发送心跳确认');
|
||||
tcpClient.sendHeartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reconnectTcp() async {
|
||||
try {
|
||||
final authCubit = context.read<AuthCubit>();
|
||||
await authCubit.reconnectAfterResume();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDataTimeout = false;
|
||||
});
|
||||
_startDataTimeoutTimer();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).translate('running_status.tcp_reconnected')),
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [RunningStatusPage] TCP重连失败: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).translate('running_status.tcp_reconnect_failed')),
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_statu
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart';
|
||||
@@ -2986,12 +2987,21 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
});
|
||||
},
|
||||
onListBox: (bool isOpen) {
|
||||
// 🔥 改动5:加载作业记录(原有逻辑保留)
|
||||
final userId =
|
||||
context.read<AppUserCubit>().state.user?.userId ??
|
||||
"";
|
||||
print('加载作业记录,当前用户ID:$userId');
|
||||
context.read<DevicesCubit>().loadWorkRecords(userId);
|
||||
// 🔥 使用新接口:根据场站ID加载作业记录
|
||||
final selectedSite = sl<SiteCubit>().state.selectedSite;
|
||||
if (selectedSite == null) {
|
||||
_showPageToast(
|
||||
message: '请先选择场站',
|
||||
type: ToastType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
print(
|
||||
'加载作业记录,当前场站ID:${selectedSite.id},场站名称:${selectedSite.siteName}',
|
||||
);
|
||||
context.read<DevicesCubit>().loadWorkRecordsBySiteId(
|
||||
selectedSite.id,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isListBoxOpen = isOpen;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/components/tcp_status_indicator.dart';
|
||||
import 'package:maibu_satabot_v2/components/device_status_modal.dart';
|
||||
import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart';
|
||||
@@ -12,6 +13,7 @@ import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workor
|
||||
import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart';
|
||||
|
||||
import '../../../v2/waring_center/presentation/pages/alarm_center_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
|
||||
class CustomMainContainer extends StatefulWidget {
|
||||
const CustomMainContainer({super.key});
|
||||
@@ -60,29 +62,33 @@ class _CustomMainContainerState extends State<CustomMainContainer>
|
||||
index: currentIndex,
|
||||
children: _buildPages(enabledTabs),
|
||||
),
|
||||
// TCP状态指示灯 - 右上角,带白色背景确保可见
|
||||
// TCP状态指示灯 - 右上角,带白色背景确保可见,点击弹出设备状态模态框
|
||||
Positioned(
|
||||
top: 40,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5), // 透明灰色背景
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TcpStatusIndicator(size: 12),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'TCP',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
],
|
||||
child: InkWell(
|
||||
onTap: () => _showDeviceStatusModal(context),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5), // 透明灰色背景
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TcpStatusIndicator(size: 12),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'TCP',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -139,4 +145,25 @@ class _CustomMainContainerState extends State<CustomMainContainer>
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 显示设备状态模态框 - 从底部滑出
|
||||
void _showDeviceStatusModal(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<DeviceStatusBloc>(),
|
||||
child: const DeviceStatusModal(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
import '../../domain/entities/video_stream_entity.dart';
|
||||
import '../../domain/entities/flight_task_entity.dart';
|
||||
|
||||
abstract class DroneStationDataSource {
|
||||
Future<List<DroneStationEntity>> getDroneStationList(int siteId);
|
||||
@@ -11,4 +12,9 @@ abstract class DroneStationDataSource {
|
||||
String qualityType = 'adaptive',
|
||||
int videoExpire = 7200,
|
||||
});
|
||||
Future<Map<String, List<FlightTaskEntity>?>> getFlightTasks({
|
||||
required List<String> sns,
|
||||
required int beginAt,
|
||||
required int endAt,
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||
import '../datasources/drone_station_datasource.dart';
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
import '../../domain/entities/video_stream_entity.dart';
|
||||
import '../../domain/entities/flight_task_entity.dart';
|
||||
|
||||
class DroneStationDataSourceImpl implements DroneStationDataSource {
|
||||
final Dio dio;
|
||||
@@ -77,11 +78,60 @@ class DroneStationDataSourceImpl implements DroneStationDataSource {
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
// 打印完整响应,方便调试
|
||||
print('=== 视频流API响应 ===');
|
||||
print('请求参数: sn=$sn, cameraIndex=$cameraIndex, cameraPosition=$cameraPosition');
|
||||
print('完整响应: ${responseData}');
|
||||
|
||||
if (responseData['code'] != 200) {
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
return VideoStreamEntity.fromJson(responseData['data']);
|
||||
final data = responseData['data'];
|
||||
print('视频流URL: ${data['url']}');
|
||||
print('视频流URL Type: ${data['url_type']}');
|
||||
|
||||
return VideoStreamEntity.fromJson(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, List<FlightTaskEntity>?>> getFlightTasks({
|
||||
required List<String> sns,
|
||||
required int beginAt,
|
||||
required int endAt,
|
||||
}) async {
|
||||
final response = await dio.post(
|
||||
HttpApiConsts.getFlightTask,
|
||||
data: {
|
||||
'sns': sns,
|
||||
'beginAt': beginAt,
|
||||
'endAt': endAt,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('网络请求失败: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData['code'] != 200) {
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
final data = responseData['data'] as Map<String, dynamic>;
|
||||
final result = <String, List<FlightTaskEntity>?>{};
|
||||
|
||||
data.forEach((sn, value) {
|
||||
if (value != null && value['list'] != null) {
|
||||
final list = value['list'] as List<dynamic>;
|
||||
result[sn] = list.map((item) => FlightTaskEntity.fromJson(item)).toList();
|
||||
} else {
|
||||
result[sn] = null;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
class FlightTaskEntity {
|
||||
final String name;
|
||||
final String uuid;
|
||||
final String taskType;
|
||||
final String status;
|
||||
final String sn;
|
||||
final String landingDockSn;
|
||||
final String beginAt;
|
||||
final String endAt;
|
||||
final String runAt;
|
||||
final String completedAt;
|
||||
final String waylineUuid;
|
||||
final int folderId;
|
||||
final int currentWaypointIndex;
|
||||
final int totalWaypoints;
|
||||
final String mediaUploadStatus;
|
||||
final String resumableStatus;
|
||||
final bool isBreakPointResume;
|
||||
final dynamic operations;
|
||||
final dynamic exceptions;
|
||||
|
||||
FlightTaskEntity({
|
||||
required this.name,
|
||||
required this.uuid,
|
||||
required this.taskType,
|
||||
required this.status,
|
||||
required this.sn,
|
||||
required this.landingDockSn,
|
||||
required this.beginAt,
|
||||
required this.endAt,
|
||||
required this.runAt,
|
||||
required this.completedAt,
|
||||
required this.waylineUuid,
|
||||
required this.folderId,
|
||||
required this.currentWaypointIndex,
|
||||
required this.totalWaypoints,
|
||||
required this.mediaUploadStatus,
|
||||
required this.resumableStatus,
|
||||
required this.isBreakPointResume,
|
||||
this.operations,
|
||||
this.exceptions,
|
||||
});
|
||||
|
||||
factory FlightTaskEntity.fromJson(Map<String, dynamic> json) {
|
||||
return FlightTaskEntity(
|
||||
name: json['name'] ?? '',
|
||||
uuid: json['uuid'] ?? '',
|
||||
taskType: json['task_type'] ?? '',
|
||||
status: json['status'] ?? '',
|
||||
sn: json['sn'] ?? '',
|
||||
landingDockSn: json['landing_dock_sn'] ?? '',
|
||||
beginAt: json['begin_at'] ?? '',
|
||||
endAt: json['end_at'] ?? '',
|
||||
runAt: json['run_at'] ?? '',
|
||||
completedAt: json['completed_at'] ?? '',
|
||||
waylineUuid: json['wayline_uuid'] ?? '',
|
||||
folderId: json['folder_id'] ?? 0,
|
||||
currentWaypointIndex: json['current_waypoint_index'] ?? 0,
|
||||
totalWaypoints: json['total_waypoints'] ?? 0,
|
||||
mediaUploadStatus: json['media_upload_status'] ?? '',
|
||||
resumableStatus: json['resumable_status'] ?? '',
|
||||
isBreakPointResume: json['is_break_point_resume'] ?? false,
|
||||
operations: json['operations'],
|
||||
exceptions: json['exceptions'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'uuid': uuid,
|
||||
'task_type': taskType,
|
||||
'status': status,
|
||||
'sn': sn,
|
||||
'landing_dock_sn': landingDockSn,
|
||||
'begin_at': beginAt,
|
||||
'end_at': endAt,
|
||||
'run_at': runAt,
|
||||
'completed_at': completedAt,
|
||||
'wayline_uuid': waylineUuid,
|
||||
'folder_id': folderId,
|
||||
'current_waypoint_index': currentWaypointIndex,
|
||||
'total_waypoints': totalWaypoints,
|
||||
'media_upload_status': mediaUploadStatus,
|
||||
'resumable_status': resumableStatus,
|
||||
'is_break_point_resume': isBreakPointResume,
|
||||
'operations': operations,
|
||||
'exceptions': exceptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../../components/tcp_status_indicator.dart';
|
||||
import '../../../../../components/device_status_modal.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/device_status_bloc.dart' as DeviceListBloc;
|
||||
import '../bloc/device_status_event.dart' as DeviceListEvent;
|
||||
import '../bloc/device_status_state.dart' as DeviceListState;
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
import '../bloc/drone_station_bloc.dart';
|
||||
import '../bloc/drone_station_event.dart';
|
||||
import '../bloc/drone_station_state.dart';
|
||||
@@ -27,7 +29,8 @@ class DeviceStatusPage extends StatelessWidget {
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) =>
|
||||
sl<DeviceStatusBloc>()..add(DeviceStatusLoadData(siteId: siteId)),
|
||||
sl<DeviceListBloc.DeviceStatusBloc>()
|
||||
..add(DeviceListEvent.DeviceStatusLoadData(siteId: siteId)),
|
||||
child: const DeviceStatusView(),
|
||||
);
|
||||
}
|
||||
@@ -47,24 +50,28 @@ class DeviceStatusView extends StatelessWidget {
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F7F7),
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildAppBar(),
|
||||
_buildSearchBar(context),
|
||||
_buildTypeFilterBar(context),
|
||||
Expanded(child: _buildContent(context, state)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child:
|
||||
BlocBuilder<
|
||||
DeviceListBloc.DeviceStatusBloc,
|
||||
DeviceListState.DeviceStatusState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildAppBar(context),
|
||||
_buildSearchBar(context),
|
||||
_buildTypeFilterBar(context),
|
||||
Expanded(child: _buildContent(context, state)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar() {
|
||||
Widget _buildAppBar(BuildContext context) {
|
||||
return Container(
|
||||
height: 44.0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -92,7 +99,10 @@ class DeviceStatusView extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
),
|
||||
child: const TcpStatusIndicator(size: 12),
|
||||
child: TcpStatusIndicator(
|
||||
size: 12,
|
||||
onTap: () => _showDeviceStatusModal(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -148,7 +158,9 @@ class DeviceStatusView extends StatelessWidget {
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
|
||||
onSubmitted: (value) {
|
||||
// 🔥 点击键盘确定键时触发搜索
|
||||
context.read<DeviceStatusBloc>().add(DeviceStatusSearch(value));
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
DeviceListEvent.DeviceStatusSearch(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -157,10 +169,13 @@ class DeviceStatusView extends StatelessWidget {
|
||||
Widget _buildTypeFilterBar(BuildContext context) {
|
||||
final types = ['全部', '机器人', '无人机机场', '逆变器', '汇流箱', '组件', '监控'];
|
||||
|
||||
return BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
return BlocBuilder<
|
||||
DeviceListBloc.DeviceStatusBloc,
|
||||
DeviceListState.DeviceStatusState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
String selectedType = '全部';
|
||||
if (state is DeviceStatusLoaded) {
|
||||
if (state is DeviceListState.DeviceStatusLoaded) {
|
||||
selectedType = state.selectedType;
|
||||
}
|
||||
|
||||
@@ -178,8 +193,8 @@ class DeviceStatusView extends StatelessWidget {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<DeviceStatusBloc>().add(
|
||||
DeviceStatusChangeType(type),
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
DeviceListEvent.DeviceStatusChangeType(type),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
@@ -219,14 +234,19 @@ class DeviceStatusView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, DeviceStatusState state) {
|
||||
Widget _buildContent(
|
||||
BuildContext context,
|
||||
DeviceListState.DeviceStatusState state,
|
||||
) {
|
||||
// 如果选择的是"机器人",显示机器人专属页面
|
||||
if (state is DeviceStatusLoaded && state.selectedType == '机器人') {
|
||||
if (state is DeviceListState.DeviceStatusLoaded &&
|
||||
state.selectedType == '机器人') {
|
||||
return const RobotListPage();
|
||||
}
|
||||
|
||||
// 如果选择的是"无人机机场",显示机场列表
|
||||
if (state is DeviceStatusLoaded && state.selectedType == '无人机机场') {
|
||||
if (state is DeviceListState.DeviceStatusLoaded &&
|
||||
state.selectedType == '无人机机场') {
|
||||
return _buildDroneStationList(context);
|
||||
}
|
||||
|
||||
@@ -234,14 +254,17 @@ class DeviceStatusView extends StatelessWidget {
|
||||
return _buildDeviceList(context, state);
|
||||
}
|
||||
|
||||
Widget _buildDeviceList(BuildContext context, DeviceStatusState state) {
|
||||
if (state is DeviceStatusLoading) {
|
||||
Widget _buildDeviceList(
|
||||
BuildContext context,
|
||||
DeviceListState.DeviceStatusState state,
|
||||
) {
|
||||
if (state is DeviceListState.DeviceStatusLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF165DFF)),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DeviceStatusError) {
|
||||
if (state is DeviceListState.DeviceStatusError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -255,8 +278,8 @@ class DeviceStatusView extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<DeviceStatusBloc>().add(
|
||||
const DeviceStatusLoadData(),
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
const DeviceListEvent.DeviceStatusLoadData(),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -270,7 +293,7 @@ class DeviceStatusView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DeviceStatusLoaded) {
|
||||
if (state is DeviceListState.DeviceStatusLoaded) {
|
||||
// 🔥 先根据类型过滤
|
||||
List filteredByType = state.devices;
|
||||
if (state.selectedType != '全部') {
|
||||
@@ -292,7 +315,9 @@ class DeviceStatusView extends StatelessWidget {
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<DeviceStatusBloc>().add(const DeviceStatusRefresh());
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
const DeviceListEvent.DeviceStatusRefresh(),
|
||||
);
|
||||
},
|
||||
color: const Color(0xFF165DFF),
|
||||
child: ListView(
|
||||
@@ -441,7 +466,7 @@ class DeviceStatusView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusCard(DeviceStatusLoaded state) {
|
||||
Widget _buildStatusCard(DeviceListState.DeviceStatusLoaded state) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -533,4 +558,25 @@ class DeviceStatusView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 显示设备状态模态框 - 从底部滑出
|
||||
void _showDeviceStatusModal(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<DeviceStatusBloc>(),
|
||||
child: const DeviceStatusModal(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/flight_task_entity.dart';
|
||||
|
||||
/// 无人机任务与航线控制页面
|
||||
class DroneMissionControlPage extends StatelessWidget {
|
||||
const DroneMissionControlPage({super.key});
|
||||
class DroneMissionControlPage extends StatefulWidget {
|
||||
final List<FlightTaskEntity>? selectedTasks;
|
||||
|
||||
const DroneMissionControlPage({super.key, this.selectedTasks});
|
||||
|
||||
@override
|
||||
State<DroneMissionControlPage> createState() => _DroneMissionControlPageState();
|
||||
}
|
||||
|
||||
class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
FlightTaskEntity? _currentTask;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.selectedTasks != null && widget.selectedTasks!.isNotEmpty) {
|
||||
_currentTask = widget.selectedTasks!.first;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -78,44 +96,104 @@ class DroneMissionControlPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow('任务名称', '逆变器区巡检任务'),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_buildInfoColumn('任务编号', 'UAV-2025052001'),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'进行中',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
if (_currentTask != null) ...[
|
||||
_buildInfoRow('任务名称', _currentTask!.name),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_buildInfoColumn('任务ID', _currentTask!.uuid.substring(0, 8)),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(_currentTask!.status).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
_currentTask!.status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _getStatusColor(_currentTask!.status),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('巡检区域', '逆变器区A区'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('飞行高度', '80 m'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('飞行速度', '8.0 m/s'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('预计时长', '26 min'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('电量预估', '68% (可飞行 22 min)'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('设备序列号', _currentTask!.sn),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('任务类型', _currentTask!.taskType),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('开始时间', _formatDateTime(_currentTask!.beginAt)),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('结束时间', _formatDateTime(_currentTask!.endAt)),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('航点数量', '${_currentTask!.totalWaypoints}'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('媒体上传', _currentTask!.mediaUploadStatus),
|
||||
] else ...[
|
||||
_buildInfoRow('任务名称', '逆变器区巡检任务'),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_buildInfoColumn('任务编号', 'UAV-2025052001'),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'进行中',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('巡检区域', '逆变器区A区'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('飞行高度', '80 m'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('飞行速度', '8.0 m/s'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('预计时长', '26 min'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('电量预估', '68% (可飞行 22 min)'),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'success':
|
||||
return const Color(0xFF00B42A);
|
||||
case 'failed':
|
||||
return const Color(0xFFF53F3F);
|
||||
case 'running':
|
||||
return const Color(0xFF165DFF);
|
||||
default:
|
||||
return const Color(0xFF86909C);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDateTime(String dateTimeStr) {
|
||||
try {
|
||||
final dateTime = DateTime.parse(dateTimeStr);
|
||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildRouteMap() {
|
||||
return Container(
|
||||
height: 200,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:volc_engine_rtc/volc_engine_rtc.dart' as volc;
|
||||
@@ -11,6 +12,7 @@ import '../bloc/drone_station_state.dart';
|
||||
import 'drone_video_control_page.dart';
|
||||
import 'drone_mission_control_page.dart';
|
||||
import 'drone_monitor_page.dart';
|
||||
import '../widgets/flight_task_selector_modal.dart';
|
||||
|
||||
// SDK 类型枚举
|
||||
enum RtcSdkType { volcengine, agora }
|
||||
@@ -47,6 +49,10 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
// Agora RTC
|
||||
agora.RtcEngine? _floatingAgoraEngine;
|
||||
|
||||
// 加载超时计时器
|
||||
Timer? _floatingLoadingTimer;
|
||||
static const _floatingLoadingTimeout = Duration(seconds: 15);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -63,6 +69,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
_destroyFloatingRtcEngine();
|
||||
_floatingLoadingTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -125,11 +132,13 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
listener: (context, state) {
|
||||
// 监听视频流加载状态
|
||||
if (state is VideoStreamLoaded) {
|
||||
_floatingLoadingTimer?.cancel();
|
||||
setState(() {
|
||||
_floatingVideoStream = state.videoStream;
|
||||
});
|
||||
_initFloatingRtcEngine();
|
||||
} else if (state is VideoStreamError) {
|
||||
_floatingLoadingTimer?.cancel();
|
||||
setState(() {
|
||||
_floatingErrorMessage = state.message;
|
||||
_isFloatingLoading = false;
|
||||
@@ -518,12 +527,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
label: '任务下发',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DroneMissionControlPage(),
|
||||
),
|
||||
);
|
||||
_showFlightTaskSelector();
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -535,6 +539,16 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
}
|
||||
|
||||
void _goToMonitor() {
|
||||
// 从当前状态中获取摄像头索引
|
||||
final currentState = _bloc.state;
|
||||
String cameraIndex = '165-0-7'; // 默认值
|
||||
|
||||
if (currentState is UAVDetailLoaded &&
|
||||
currentState.detail.gatewayCameraList != null &&
|
||||
currentState.detail.gatewayCameraList!.isNotEmpty) {
|
||||
cameraIndex = currentState.detail.gatewayCameraList!.first.cameraIndex;
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
@@ -559,7 +573,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DroneMonitorPage(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
cameraIndex: '165-0-7',
|
||||
cameraIndex: cameraIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -589,36 +603,37 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_loadFloatingVideoStream();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.picture_in_picture, color: Color(0xFF165DFF)),
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
'悬浮观看',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Icon(Icons.arrow_forward_ios, color: Color(0xFF86909C)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 悬浮观看功能已禁用
|
||||
// GestureDetector(
|
||||
// onTap: () {
|
||||
// Navigator.pop(context);
|
||||
// _loadFloatingVideoStream();
|
||||
// },
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.all(16),
|
||||
// decoration: BoxDecoration(
|
||||
// color: const Color(0xFFF2F3F5),
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// child: const Row(
|
||||
// children: [
|
||||
// Icon(Icons.picture_in_picture, color: Color(0xFF165DFF)),
|
||||
// SizedBox(width: 16),
|
||||
// Text(
|
||||
// '悬浮观看',
|
||||
// style: TextStyle(
|
||||
// fontSize: 16,
|
||||
// color: Color(0xFF1D2129),
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// Spacer(),
|
||||
// Icon(Icons.arrow_forward_ios, color: Color(0xFF86909C)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -626,6 +641,46 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 显示飞行任务选择器
|
||||
void _showFlightTaskSelector() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.7,
|
||||
minChildSize: 0.5,
|
||||
maxChildSize: 0.95,
|
||||
builder: (context, scrollController) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: FlightTaskSelectorModal(
|
||||
currentGatewaySn: widget.station.gatewaySn,
|
||||
onTaskSelected: (tasks) {
|
||||
if (tasks.isNotEmpty) {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
DroneMissionControlPage(selectedTasks: tasks),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 悬浮视频监控组件
|
||||
Widget _buildFloatingMonitor() {
|
||||
if (!showFloatingMonitor) return const SizedBox();
|
||||
@@ -858,6 +913,18 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
|
||||
// 加载悬浮视频流
|
||||
void _loadFloatingVideoStream() {
|
||||
_floatingLoadingTimer?.cancel();
|
||||
|
||||
// 从当前状态中获取摄像头索引
|
||||
final currentState = _bloc.state;
|
||||
String cameraIndex = '165-0-7'; // 默认值
|
||||
|
||||
if (currentState is UAVDetailLoaded &&
|
||||
currentState.detail.gatewayCameraList != null &&
|
||||
currentState.detail.gatewayCameraList!.isNotEmpty) {
|
||||
cameraIndex = currentState.detail.gatewayCameraList!.first.cameraIndex;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isFloatingLoading = true;
|
||||
_floatingErrorMessage = null;
|
||||
@@ -866,10 +933,22 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
|
||||
_destroyFloatingRtcEngine();
|
||||
|
||||
// 设置加载超时计时器
|
||||
_floatingLoadingTimer = Timer(_floatingLoadingTimeout, () {
|
||||
if (!mounted) return;
|
||||
if (_isFloatingLoading) {
|
||||
debugPrint('⚠️ 悬浮窗视频加载超时');
|
||||
setState(() {
|
||||
_isFloatingLoading = false;
|
||||
_floatingErrorMessage = '视频加载超时,请检查网络连接或点击刷新重试';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_bloc.add(
|
||||
VideoStreamLoad(
|
||||
sn: widget.station.gatewaySn,
|
||||
cameraIndex: '165-0-7',
|
||||
cameraIndex: cameraIndex,
|
||||
cameraPosition: isFloatingIndoor ? 'indoor' : 'outdoor',
|
||||
),
|
||||
);
|
||||
@@ -1007,6 +1086,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
},
|
||||
onConnectionStateChanged: (state, reason) {
|
||||
debugPrint(
|
||||
'Volc 悬浮窗 Connection State Changed: $state, reason: $reason',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
_floatingRtcEngine = await volc.RTCEngine.createRTCEngine(
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../../../core/consts/http_api_consts.dart';
|
||||
import '../../domain/entities/flight_task_entity.dart';
|
||||
|
||||
class FlightTaskSelectorModal extends StatefulWidget {
|
||||
final String currentGatewaySn;
|
||||
final Function(List<FlightTaskEntity>) onTaskSelected;
|
||||
|
||||
const FlightTaskSelectorModal({
|
||||
super.key,
|
||||
required this.currentGatewaySn,
|
||||
required this.onTaskSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FlightTaskSelectorModal> createState() => _FlightTaskSelectorModalState();
|
||||
}
|
||||
|
||||
class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
|
||||
DateTimeRange? _selectedDateRange;
|
||||
String? _selectedDeviceSn;
|
||||
Map<String, List<FlightTaskEntity>?> _taskData = {};
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
final Dio _dio = Dio();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDeviceSn = widget.currentGatewaySn;
|
||||
_setDefaultDateRange();
|
||||
}
|
||||
|
||||
void _setDefaultDateRange() {
|
||||
final now = DateTime.now();
|
||||
final todayStart = DateTime(now.year, now.month, now.day);
|
||||
final todayEnd = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
_selectedDateRange = DateTimeRange(start: todayStart, end: todayEnd);
|
||||
}
|
||||
|
||||
Future<void> _selectDateRange(BuildContext context) async {
|
||||
final picked = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2030),
|
||||
initialDateRange: _selectedDateRange,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDateRange = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadTasks() async {
|
||||
if (_selectedDateRange == null || _selectedDeviceSn == null) {
|
||||
setState(() {
|
||||
_errorMessage = '请选择时间和设备';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final beginAt = _selectedDateRange!.start.millisecondsSinceEpoch ~/ 1000;
|
||||
final endAt = _selectedDateRange!.end.millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
final response = await _dio.post(
|
||||
HttpApiConsts.getFlightTask,
|
||||
data: {
|
||||
'sns': [_selectedDeviceSn!],
|
||||
'beginAt': beginAt,
|
||||
'endAt': endAt,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('网络请求失败');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData['code'] != 200) {
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
final data = responseData['data'] as Map<String, dynamic>;
|
||||
final result = <String, List<FlightTaskEntity>?>{};
|
||||
|
||||
data.forEach((sn, value) {
|
||||
if (value != null && value['list'] != null) {
|
||||
final list = value['list'] as List<dynamic>;
|
||||
result[sn] = list.map((item) => FlightTaskEntity.fromJson(item)).toList();
|
||||
} else {
|
||||
result[sn] = null;
|
||||
}
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_taskData = result;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = '加载失败: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'选择飞行任务',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTimeSelector(),
|
||||
const SizedBox(height: 12),
|
||||
_buildDeviceSelector(),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _loadTasks,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text('查询任务'),
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(color: Color(0xFFF53F3F)),
|
||||
),
|
||||
],
|
||||
if (_taskData.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Flexible(
|
||||
child: _buildTaskList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeSelector() {
|
||||
return GestureDetector(
|
||||
onTap: () => _selectDateRange(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, color: Color(0xFF86909C)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedDateRange != null
|
||||
? '${_formatDate(_selectedDateRange!.start)} - ${_formatDate(_selectedDateRange!.end)}'
|
||||
: '选择时间范围',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceSelector() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.devices, color: Color(0xFF86909C)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedDeviceSn ?? '未知设备',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTaskList() {
|
||||
final tasks = _taskData[_selectedDeviceSn];
|
||||
|
||||
if (tasks == null || tasks.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Text('暂无任务数据'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = tasks[index];
|
||||
return _buildTaskItem(task);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTaskItem(FlightTaskEntity task) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onTaskSelected([task]);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE5E6EB)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
task.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'类型: ${task.taskType}',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(task.status).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
task.status,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _getStatusColor(task.status),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'开始: ${_formatDateTime(task.beginAt)}',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||
),
|
||||
Text(
|
||||
'结束: ${_formatDateTime(task.endAt)}',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'success':
|
||||
return const Color(0xFF00B42A);
|
||||
case 'failed':
|
||||
return const Color(0xFFF53F3F);
|
||||
case 'running':
|
||||
return const Color(0xFF165DFF);
|
||||
default:
|
||||
return const Color(0xFF86909C);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _formatDateTime(String dateTimeStr) {
|
||||
try {
|
||||
final dateTime = DateTime.parse(dateTimeStr);
|
||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/quick_ent
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_trend_chart.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/plant_overview_card.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/tcp_status_indicator.dart';
|
||||
import 'package:maibu_satabot_v2/components/device_status_modal.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
|
||||
class HomeV2Page extends StatefulWidget {
|
||||
const HomeV2Page({super.key});
|
||||
@@ -40,7 +42,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)),
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message),
|
||||
const SizedBox(height: 16),
|
||||
@@ -81,9 +87,12 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
child: StreamBuilder<SiteState>(
|
||||
stream: sl<SiteCubit>().stream,
|
||||
builder: (context, snapshot) {
|
||||
final siteState = snapshot.data ?? sl<SiteCubit>().state;
|
||||
final siteState =
|
||||
snapshot.data ??
|
||||
sl<SiteCubit>().state;
|
||||
return Text(
|
||||
siteState.selectedSite?.siteName ?? '选择电站',
|
||||
siteState.selectedSite?.siteName ??
|
||||
'选择电站',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
@@ -96,12 +105,18 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_drop_down, size: 20, color: Color(0xFF1D2129)),
|
||||
const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
size: 20,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const TcpStatusIndicator(),
|
||||
TcpStatusIndicator(
|
||||
onTap: () => _showDeviceStatusModal(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -117,7 +132,10 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -125,7 +143,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_queue, size: 16, color: Color(0xFF165DFF)),
|
||||
const Icon(
|
||||
Icons.cloud_queue,
|
||||
size: 16,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'多云 28°C',
|
||||
@@ -141,7 +163,10 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF0F0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -149,7 +174,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF53F3F)),
|
||||
const Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
size: 16,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
@@ -161,7 +190,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFFF53F3F)),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -173,7 +206,9 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
const SizedBox(height: 10),
|
||||
StatsGrid(data: state.homeData),
|
||||
const SizedBox(height: 10),
|
||||
PlantOverviewCard(overview: state.homeData.plantOverview),
|
||||
PlantOverviewCard(
|
||||
overview: state.homeData.plantOverview,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
WorkOrderCard(stats: state.homeData.workOrderStats),
|
||||
const SizedBox(height: 10),
|
||||
@@ -182,7 +217,8 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
PowerTrendChart(
|
||||
data: state.homeData.powerTrendData,
|
||||
trendType: state.trendType,
|
||||
onToggleType: (type) => _bloc.add(HomeV2ToggleTrendType(type)),
|
||||
onToggleType: (type) =>
|
||||
_bloc.add(HomeV2ToggleTrendType(type)),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
]),
|
||||
@@ -215,14 +251,14 @@ 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('暂无可用电站')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('暂无可用电站')));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,7 +273,7 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
bloc: _bloc,
|
||||
builder: (context, state) {
|
||||
if (state is! HomeV2Loaded) return Container();
|
||||
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
constraints: BoxConstraints(
|
||||
@@ -262,20 +298,24 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
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
|
||||
subtitle:
|
||||
site.siteCode != null && site.siteCode!.isNotEmpty
|
||||
? Text('编号: ${site.siteCode}')
|
||||
: null,
|
||||
trailing: isSelected
|
||||
? const Icon(Icons.check, color: Color(0xFF165DFF))
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Color(0xFF165DFF),
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
// 更新全局选中的场站
|
||||
sl<SiteCubit>().selectSite(site);
|
||||
Navigator.pop(context);
|
||||
|
||||
|
||||
// 切换场站后,重新加载首页数据
|
||||
_bloc.add(const HomeV2LoadData());
|
||||
},
|
||||
@@ -291,4 +331,25 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 显示设备状态模态框 - 从底部滑出
|
||||
void _showDeviceStatusModal(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<DeviceStatusBloc>(),
|
||||
child: const DeviceStatusModal(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import 'package:maibu_satabot_v2/core/network/tcp/tcp_status_cubit.dart';
|
||||
|
||||
/// TCP 连接状态指示灯组件
|
||||
class TcpStatusIndicator extends StatefulWidget {
|
||||
const TcpStatusIndicator({super.key});
|
||||
final VoidCallback? onTap; // 点击回调
|
||||
|
||||
const TcpStatusIndicator({super.key, this.onTap});
|
||||
|
||||
@override
|
||||
State<TcpStatusIndicator> createState() => _TcpStatusIndicatorState();
|
||||
@@ -21,16 +23,17 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tcpStatusCubit = GetIt.I<TcpStatusCubit>();
|
||||
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
)..repeat(reverse: true);
|
||||
|
||||
_animation = Tween<double>(begin: 0.6, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: 0.6,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
|
||||
_tcpStatusCubit.stream.listen((state) {
|
||||
if (state.status != TcpConnectionStatus.disconnected) {
|
||||
_hasActivity = true;
|
||||
@@ -81,28 +84,32 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation.value : 1.0;
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation.value : 1.0;
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1730,7 +1730,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
|
||||
@@ -56,6 +56,7 @@ dependencies:
|
||||
bloc: ^9.2.0
|
||||
image: ^4.1.7 # 用于图片格式转换
|
||||
http: ^1.1.0
|
||||
xml: ^6.5.0
|
||||
logger: ^2.0.0 # 最新版本可查 pub.dev
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user