对接路径规划的列表(根据场站的下的)

优化无人机机场的监控视频的显示视频流畅度提升80%
添加首页tcp指示灯的机器状态信息展示功能。(暂不支持在此页面上设备的切换显示)
This commit is contained in:
2026-06-09 14:39:55 +08:00
parent ff4a3836c7
commit 058a5d9588
26 changed files with 3634 additions and 1028 deletions

View File

@@ -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": {

View File

@@ -98,7 +98,9 @@
"value": "数值",
"initialized": "已初始化",
"no_data": "暂无数据",
"voltage": "电压"
"voltage": "电压",
"tcp_reconnected": "TCP连接已恢复",
"tcp_reconnect_failed": "TCP重连失败,请检查网络"
},
"machine_details": {

File diff suppressed because it is too large Load Diff

View File

@@ -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,
),
]
: [],
),
);
},
),
),
);
}

View File

@@ -36,4 +36,7 @@ class HttpApiConsts {
// 获取机器人列表
static const String getRobotList = "$baseUrl/iot/device/getSiteList";
// 获取飞行任务列表
static const String getFlightTask = "$baseUrl/iot/UAV/getFlightTask";
}

View File

@@ -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(

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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});
}

View File

@@ -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()));
}
}
}

View File

@@ -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',
);
}
//获取已到达的点的经纬度

View File

@@ -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,
),
);
}
}
}
}

View File

@@ -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;

View File

@@ -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(),
);
},
);
}
}

View File

@@ -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,
});
}

View File

@@ -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;
}
}

View File

@@ -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,
};
}
}

View File

@@ -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(),
);
},
);
}
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -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;
}
}
}

View File

@@ -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(),
);
},
);
}
}

View File

@@ -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,
),
]
: [],
),
);
},
),
),
);
}

View File

@@ -1730,7 +1730,7 @@ packages:
source: hosted
version: "1.1.0"
xml:
dependency: transitive
dependency: "direct main"
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"

View File

@@ -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