机器人的列表接口
This commit is contained in:
@@ -19,16 +19,21 @@ class HttpApiConsts {
|
||||
// 获取光伏电站列表
|
||||
static const String getSiteList = "$baseUrl/system/site/list";
|
||||
|
||||
// 获取场站下的设备列表
|
||||
static const String getSiteDeviceList = "$baseUrl/iot/device/getSiteList";
|
||||
|
||||
// 获取场站下的无人机机场列表
|
||||
static const String getSiteUAVList = "$baseUrl/iot/UAV/getSiteUAVList";
|
||||
|
||||
// 获取UAV状态详情
|
||||
static const String getUAVState = "$baseUrl/iot/UAV/getUAVState";
|
||||
|
||||
// 切换摄像头获取视频流
|
||||
static const String changeCamera = "$baseUrl/iot/UAV/changeCamera";
|
||||
|
||||
// 获取设备位置
|
||||
static const String getDeviceLocation = "$baseUrl/iot/device/userDevice";
|
||||
|
||||
|
||||
|
||||
// 获取机器人列表
|
||||
static const String getRobotList = "$baseUrl/iot/device/getSiteList";
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/router/route_paths.dart';
|
||||
import 'package:maibu_satabot_v2/features/ai/presentation/routes/ai_routes.dart';
|
||||
@@ -16,6 +17,13 @@ import '../../features/main_container/presentation/main_wrapper.dart';
|
||||
import 'go_router_refresh_stream.dart';
|
||||
|
||||
GoRouter createRouter(AuthCubit authCubit) {
|
||||
// 调试:打印所有路由
|
||||
debugPrint('🔍 [Router] 开始创建路由配置...');
|
||||
debugPrint('🔍 [Router] AlarmCenterRoutes.routes 数量: ${AlarmCenterRoutes.routes.length}');
|
||||
for (var route in AlarmCenterRoutes.routes) {
|
||||
debugPrint('🔍 [Router] 路由: ${route.toString()}');
|
||||
}
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: RoutePaths.login,
|
||||
refreshListenable: GoRouterRefreshStream(authCubit.stream),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../../../core/consts/http_api_consts.dart';
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
import '../../domain/entities/device_status_entity.dart';
|
||||
import '../models/device_data_model.dart';
|
||||
@@ -6,17 +8,22 @@ import '../models/device_status_data_model.dart';
|
||||
/// 设备远程数据源抽象接口
|
||||
abstract class DeviceRemoteDataSource {
|
||||
Future<DeviceStatusDataModel> getDeviceStatus();
|
||||
Future<List<DeviceDataModel>> getDeviceList({String? typeFilter});
|
||||
Future<List<DeviceDataModel>> getDeviceList({
|
||||
int? siteId,
|
||||
String? typeFilter,
|
||||
});
|
||||
}
|
||||
|
||||
/// 设备远程数据源实现类 - 模拟从 API 获取数据
|
||||
/// 设备远程数据源实现类 - 从真实 API 获取数据
|
||||
class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
final Dio dio;
|
||||
|
||||
DeviceRemoteDataSourceImpl(this.dio);
|
||||
|
||||
@override
|
||||
Future<DeviceStatusDataModel> getDeviceStatus() async {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 模拟数据
|
||||
return const DeviceStatusDataModel(
|
||||
total: 136,
|
||||
online: 122,
|
||||
@@ -26,11 +33,119 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceDataModel>> getDeviceList({String? typeFilter}) async {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Future<List<DeviceDataModel>> getDeviceList({
|
||||
int? siteId,
|
||||
String? typeFilter,
|
||||
}) async {
|
||||
if (siteId == null) {
|
||||
return _getMockDevices(typeFilter);
|
||||
}
|
||||
|
||||
// 模拟数据 - 根据设计图
|
||||
try {
|
||||
final List<Future<List<DeviceDataModel>>> futures = [
|
||||
_fetchDevicesFromAPI(siteId),
|
||||
_fetchUAVDevicesFromAPI(siteId),
|
||||
];
|
||||
|
||||
final results = await Future.wait(futures);
|
||||
|
||||
final allDevices = <DeviceDataModel>[];
|
||||
for (var result in results) {
|
||||
allDevices.addAll(result);
|
||||
}
|
||||
|
||||
if (typeFilter != null && typeFilter != '全部') {
|
||||
return allDevices.where((device) => device.type == typeFilter).toList();
|
||||
}
|
||||
|
||||
return allDevices;
|
||||
} catch (e) {
|
||||
return _getMockDevices(typeFilter);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DeviceDataModel>> _fetchDevicesFromAPI(int siteId) async {
|
||||
final response = await dio.get(
|
||||
HttpApiConsts.getSiteDeviceList,
|
||||
queryParameters: {'siteId': siteId, 'pageSize': 9999, 'pageNum': 1},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('网络请求失败: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData['code'] != 200) {
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
final List<dynamic> rows = responseData['rows'] ?? [];
|
||||
return rows.map((item) => _parseDeviceFromJson(item)).toList();
|
||||
}
|
||||
|
||||
Future<List<DeviceDataModel>> _fetchUAVDevicesFromAPI(int siteId) async {
|
||||
final response = await dio.get(
|
||||
HttpApiConsts.getSiteUAVList,
|
||||
queryParameters: {'siteId': siteId},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('网络请求失败: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData['code'] != 200) {
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
final List<dynamic> rows = responseData['rows'] ?? [];
|
||||
return rows.map((item) => _parseUAVDeviceFromJson(item)).toList();
|
||||
}
|
||||
|
||||
DeviceDataModel _parseDeviceFromJson(Map<String, dynamic> json) {
|
||||
return DeviceDataModel(
|
||||
deviceId: json['deviceId']?.toString() ?? '',
|
||||
name: json['deviceName'] ?? json['name'] ?? '',
|
||||
type: json['deviceTypeName'] ?? json['type'] ?? '未知设备',
|
||||
status: _getStatusText(json['status'] ?? 0),
|
||||
power: (json['power'] as num?)?.toDouble() ?? 0,
|
||||
todayEnergy: (json['todayEnergy'] as num?)?.toDouble() ?? 0,
|
||||
temperature: (json['temperature'] as num?)?.toDouble(),
|
||||
current: (json['current'] as num?)?.toDouble(),
|
||||
voltage: (json['voltage'] as num?)?.toDouble(),
|
||||
radiation: (json['radiation'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
DeviceDataModel _parseUAVDeviceFromJson(Map<String, dynamic> json) {
|
||||
final onlineStatus = json['onlineStatus'] ?? 0;
|
||||
return DeviceDataModel(
|
||||
deviceId: json['device_sn']?.toString() ?? '',
|
||||
name: json['callsign'] ?? '无人机机场',
|
||||
type: '无人机机场',
|
||||
status: onlineStatus == 1 ? '在线' : '离线',
|
||||
power: (json['capacity_percent'] as num?)?.toDouble() ?? 0,
|
||||
todayEnergy: 0,
|
||||
temperature: (json['environment_temperature'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusText(int status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '在线';
|
||||
case 2:
|
||||
return '离线';
|
||||
case 3:
|
||||
return '异常';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
List<DeviceDataModel> _getMockDevices(String? typeFilter) {
|
||||
final allDevices = [
|
||||
const DeviceDataModel(
|
||||
deviceId: '10001',
|
||||
@@ -85,7 +200,6 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
),
|
||||
];
|
||||
|
||||
// 根据类型过滤
|
||||
if (typeFilter != null && typeFilter != '全部') {
|
||||
return allDevices.where((device) => device.type == typeFilter).toList();
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ class DeviceRepositoryImpl implements DeviceRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceEntity>> getDeviceList({String? typeFilter}) async {
|
||||
final dataModels = await remoteDataSource.getDeviceList(typeFilter: typeFilter);
|
||||
Future<List<DeviceEntity>> getDeviceList({int? siteId, String? typeFilter}) async {
|
||||
final dataModels = await remoteDataSource.getDeviceList(siteId: siteId, typeFilter: typeFilter);
|
||||
return dataModels.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import '../entities/device_status_entity.dart';
|
||||
/// 设备仓储抽象接口
|
||||
abstract class DeviceRepository {
|
||||
Future<DeviceStatusEntity> getDeviceStatus();
|
||||
Future<List<DeviceEntity>> getDeviceList({String? typeFilter});
|
||||
Future<List<DeviceEntity>> getDeviceList({int? siteId, String? typeFilter});
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ class GetDeviceStatusDataUseCase {
|
||||
|
||||
const GetDeviceStatusDataUseCase({required this.repository});
|
||||
|
||||
Future<DeviceStatusResponse> execute({String? typeFilter}) async {
|
||||
Future<DeviceStatusResponse> execute({int? siteId, String? typeFilter}) async {
|
||||
try {
|
||||
final status = await repository.getDeviceStatus();
|
||||
final devices = await repository.getDeviceList(typeFilter: typeFilter);
|
||||
final devices = await repository.getDeviceList(siteId: siteId, typeFilter: typeFilter);
|
||||
return DeviceStatusResponse(status: status, devices: devices);
|
||||
} catch (e) {
|
||||
throw Exception('获取设备数据失败: ${e.toString()}');
|
||||
|
||||
@@ -21,11 +21,14 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
emit(const DeviceStatusLoading());
|
||||
|
||||
try {
|
||||
final response = await getDeviceStatusDataUseCase.execute();
|
||||
final response = await getDeviceStatusDataUseCase.execute(
|
||||
siteId: event.siteId,
|
||||
);
|
||||
|
||||
emit(DeviceStatusLoaded(
|
||||
deviceStatus: response.status,
|
||||
devices: response.devices,
|
||||
siteId: event.siteId,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(DeviceStatusError(e.toString()));
|
||||
@@ -41,6 +44,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
|
||||
try {
|
||||
final response = await getDeviceStatusDataUseCase.execute(
|
||||
siteId: currentState.siteId,
|
||||
typeFilter: currentState.selectedType == '全部'
|
||||
? null
|
||||
: currentState.selectedType,
|
||||
|
||||
@@ -8,7 +8,12 @@ abstract class DeviceStatusEvent extends Equatable {
|
||||
}
|
||||
|
||||
class DeviceStatusLoadData extends DeviceStatusEvent {
|
||||
const DeviceStatusLoadData();
|
||||
final int? siteId;
|
||||
|
||||
const DeviceStatusLoadData({this.siteId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [siteId];
|
||||
}
|
||||
|
||||
class DeviceStatusRefresh extends DeviceStatusEvent {
|
||||
|
||||
@@ -22,12 +22,14 @@ class DeviceStatusLoaded extends DeviceStatusState {
|
||||
final List<DeviceEntity> devices;
|
||||
final String selectedType;
|
||||
final String searchKeyword;
|
||||
final int? siteId;
|
||||
|
||||
const DeviceStatusLoaded({
|
||||
required this.deviceStatus,
|
||||
required this.devices,
|
||||
this.selectedType = '全部',
|
||||
this.searchKeyword = '',
|
||||
this.siteId,
|
||||
});
|
||||
|
||||
DeviceStatusLoaded copyWith({
|
||||
@@ -35,17 +37,19 @@ class DeviceStatusLoaded extends DeviceStatusState {
|
||||
List<DeviceEntity>? devices,
|
||||
String? selectedType,
|
||||
String? searchKeyword,
|
||||
int? siteId,
|
||||
}) {
|
||||
return DeviceStatusLoaded(
|
||||
deviceStatus: deviceStatus ?? this.deviceStatus,
|
||||
devices: devices ?? this.devices,
|
||||
selectedType: selectedType ?? this.selectedType,
|
||||
searchKeyword: searchKeyword ?? this.searchKeyword,
|
||||
siteId: siteId ?? this.siteId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [deviceStatus, devices, selectedType, searchKeyword];
|
||||
List<Object?> get props => [deviceStatus, devices, selectedType, searchKeyword, siteId];
|
||||
}
|
||||
|
||||
class DeviceStatusError extends DeviceStatusState {
|
||||
|
||||
@@ -20,8 +20,11 @@ class DeviceStatusPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedSite = sl<SiteCubit>().state.selectedSite;
|
||||
final siteId = selectedSite?.id;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => sl<DeviceStatusBloc>()..add(const DeviceStatusLoadData()),
|
||||
create: (_) => sl<DeviceStatusBloc>()..add(DeviceStatusLoadData(siteId: siteId)),
|
||||
child: const DeviceStatusView(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ import 'drone_monitor_page.dart';
|
||||
class DroneStationDetailPage extends StatefulWidget {
|
||||
final DroneStationEntity station;
|
||||
|
||||
const DroneStationDetailPage({
|
||||
super.key,
|
||||
required this.station,
|
||||
});
|
||||
const DroneStationDetailPage({super.key, required this.station});
|
||||
|
||||
@override
|
||||
State<DroneStationDetailPage> createState() => _DroneStationDetailPageState();
|
||||
@@ -24,14 +21,21 @@ class DroneStationDetailPage extends StatefulWidget {
|
||||
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
|
||||
// 悬浮视频监控状态
|
||||
bool showFloatingMonitor = false;
|
||||
bool isFloatingIndoor = true;
|
||||
Offset floatingPosition = const Offset(20, 200);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
_bloc.add(UAVDetailLoad(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
deviceSn: widget.station.deviceSn,
|
||||
));
|
||||
_bloc.add(
|
||||
UAVDetailLoad(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
deviceSn: widget.station.deviceSn,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -76,18 +80,32 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
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, style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969))),
|
||||
Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_bloc.add(UAVDetailLoad(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
deviceSn: widget.station.deviceSn,
|
||||
));
|
||||
_bloc.add(
|
||||
UAVDetailLoad(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
deviceSn: widget.station.deviceSn,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF165DFF)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
@@ -107,19 +125,81 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
}
|
||||
|
||||
Widget _buildContent(UAVDetailEntity detail) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
return Stack(
|
||||
children: [
|
||||
_buildAirportStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildDroneStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 20),
|
||||
ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAirportStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildMonitorCard(),
|
||||
const SizedBox(height: 12),
|
||||
_buildDroneStatusCard(detail),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
_buildFloatingMonitor(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonitorCard() {
|
||||
return GestureDetector(
|
||||
onTap: _goToMonitor,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1A165DFF),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.monitor, color: Colors.white, size: 28),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
'查看监控',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'实时查看机场摄像头画面',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xCCFFFFFF)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAirportStatusCard(UAVDetailEntity detail) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -127,7 +207,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2)),
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
@@ -135,28 +219,67 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text('机场状态', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||
const Text(
|
||||
'机场状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: detail.isOnline ? const Color(0xFF00B42A).withOpacity(0.1) : const Color(0xFFF53F3F).withOpacity(0.1),
|
||||
color: detail.isOnline
|
||||
? const Color(0xFF00B42A).withOpacity(0.1)
|
||||
: const Color(0xFFF53F3F).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(detail.isOnline ? '在线' : '离线', style: TextStyle(fontSize: 12, color: detail.isOnline ? const Color(0xFF00B42A) : const Color(0xFFF53F3F), fontWeight: FontWeight.w500)),
|
||||
child: Text(
|
||||
detail.isOnline ? '在线' : '离线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: detail.isOnline
|
||||
? const Color(0xFF00B42A)
|
||||
: const Color(0xFFF53F3F),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1, color: Color(0xFFF2F3F5)),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow('机场名称', detail.callsign.isNotEmpty ? detail.callsign : '未知'),
|
||||
_buildInfoRow(
|
||||
'机场名称',
|
||||
detail.callsign.isNotEmpty ? detail.callsign : '未知',
|
||||
),
|
||||
_buildInfoRow('设备序列号', detail.deviceSn),
|
||||
_buildInfoRow('网关序列号', detail.gatewaySn),
|
||||
_buildInfoRow('位置坐标', (detail.latitude != null && detail.longitude != null) ? '${detail.latitude}, ${detail.longitude}' : '未知'),
|
||||
_buildInfoRow('电量', detail.capacityPercent != null ? '${detail.capacityPercent}%' : '未知'),
|
||||
_buildInfoRow('环境温度', detail.environmentTemperature != null ? '${detail.environmentTemperature}°C' : '未知'),
|
||||
_buildInfoRow('风速', detail.windSpeed != null ? '${detail.windSpeed} m/s' : '未知'),
|
||||
_buildInfoRow(
|
||||
'位置坐标',
|
||||
(detail.latitude != null && detail.longitude != null)
|
||||
? '${detail.latitude}, ${detail.longitude}'
|
||||
: '未知',
|
||||
),
|
||||
_buildInfoRow(
|
||||
'电量',
|
||||
detail.capacityPercent != null
|
||||
? '${detail.capacityPercent}%'
|
||||
: '未知',
|
||||
),
|
||||
_buildInfoRow(
|
||||
'环境温度',
|
||||
detail.environmentTemperature != null
|
||||
? '${detail.environmentTemperature}°C'
|
||||
: '未知',
|
||||
),
|
||||
_buildInfoRow(
|
||||
'风速',
|
||||
detail.windSpeed != null ? '${detail.windSpeed} m/s' : '未知',
|
||||
),
|
||||
_buildInfoRow('降雨量', detail.rainfall ?? '未知'),
|
||||
_buildInfoRow('网络状态', detail.networkState?.toString() ?? '未知'),
|
||||
_buildPositionStateRow('位置状态', detail.positionState),
|
||||
@@ -168,45 +291,95 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
Widget _buildDroneStatusCard(UAVDetailEntity detail) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const DroneVideoControlPage()));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DroneVideoControlPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text('无人机状态', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||
const Text(
|
||||
'无人机状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: detail.isDroneOnline ? const Color(0xFF00B42A).withOpacity(0.1) : const Color(0xFFF53F3F).withOpacity(0.1),
|
||||
color: detail.isDroneOnline
|
||||
? const Color(0xFF00B42A).withOpacity(0.1)
|
||||
: const Color(0xFFF53F3F).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(detail.isDroneOnline ? '在线' : '离线', style: TextStyle(fontSize: 12, color: detail.isDroneOnline ? const Color(0xFF00B42A) : const Color(0xFFF53F3F), fontWeight: FontWeight.w500)),
|
||||
child: Text(
|
||||
detail.isDroneOnline ? '在线' : '离线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: detail.isDroneOnline
|
||||
? const Color(0xFF00B42A)
|
||||
: const Color(0xFFF53F3F),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow('无人机呼号', detail.droneCallsign.isNotEmpty ? detail.droneCallsign : '未知'),
|
||||
_buildInfoRow(
|
||||
'无人机呼号',
|
||||
detail.droneCallsign.isNotEmpty ? detail.droneCallsign : '未知',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('设备序列号', detail.deviceSn),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('电量', detail.capacityPercent != null ? '${detail.capacityPercent}%' : '未知'),
|
||||
_buildInfoRow(
|
||||
'电量',
|
||||
detail.capacityPercent != null
|
||||
? '${detail.capacityPercent}%'
|
||||
: '未知',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('高度', detail.height != null ? '${detail.height} m' : '未知'),
|
||||
_buildInfoRow(
|
||||
'高度',
|
||||
detail.height != null ? '${detail.height} m' : '未知',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('距离home点', detail.homeDistance != null ? '${detail.homeDistance} m' : '未知'),
|
||||
_buildInfoRow(
|
||||
'距离home点',
|
||||
detail.homeDistance != null ? '${detail.homeDistance} m' : '未知',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('实时电量', detail.liveCapacity != null ? '${detail.liveCapacity}%' : '未知'),
|
||||
if (detail.gatewayCameraList != null && detail.gatewayCameraList!.isNotEmpty) ...[
|
||||
_buildInfoRow(
|
||||
'实时电量',
|
||||
detail.liveCapacity != null ? '${detail.liveCapacity}%' : '未知',
|
||||
),
|
||||
if (detail.gatewayCameraList != null &&
|
||||
detail.gatewayCameraList!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildCameraListRow('网关摄像头', detail.gatewayCameraList!),
|
||||
],
|
||||
@@ -222,22 +395,63 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('快捷操作', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||
const Text(
|
||||
'快捷操作',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildQuickActionButton(icon: Icons.flight_takeoff, label: '开舱', color: const Color(0xFF165DFF), onTap: () {})),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.flight_takeoff,
|
||||
label: '开舱',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildQuickActionButton(icon: Icons.flight, label: '起飞准备', color: const Color(0xFF165DFF), onTap: () {})),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.flight,
|
||||
label: '起飞准备',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildQuickActionButton(icon: Icons.home, label: '返航', color: const Color(0xFFFF7D00), onTap: () {})),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.home,
|
||||
label: '返航',
|
||||
color: const Color(0xFFFF7D00),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildQuickActionButton(icon: Icons.monitor, label: '看监控', color: const Color(0xFF165DFF), onTap: _goToMonitor)),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.task,
|
||||
label: '任务下发',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -246,21 +460,253 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
}
|
||||
|
||||
void _goToMonitor() {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => DroneMonitorPage()));
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (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),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DroneMonitorPage(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
cameraIndex: '165-0-7',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.video_library, color: Colors.white),
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
'观看',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Icon(Icons.arrow_forward_ios, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
setState(() => showFloatingMonitor = true);
|
||||
},
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionButton({required IconData icon, required String label, required Color color, required VoidCallback onTap}) {
|
||||
// 悬浮视频监控组件
|
||||
Widget _buildFloatingMonitor() {
|
||||
if (!showFloatingMonitor) return const SizedBox();
|
||||
|
||||
return Positioned(
|
||||
left: floatingPosition.dx,
|
||||
top: floatingPosition.dy,
|
||||
child: Draggable(
|
||||
feedback: _monitorFloatCard(),
|
||||
childWhenDragging: const SizedBox(),
|
||||
onDragEnd: (details) {
|
||||
setState(() {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
floatingPosition = Offset(
|
||||
details.offset.dx.clamp(0, screenWidth - 280),
|
||||
details.offset.dy.clamp(0, screenHeight - 200),
|
||||
);
|
||||
});
|
||||
},
|
||||
child: _monitorFloatCard(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _monitorFloatCard() {
|
||||
return Container(
|
||||
width: 280,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x20000000),
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 视频区域
|
||||
Container(
|
||||
height: 140,
|
||||
color: const Color(0xFF0D1117),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.video_library,
|
||||
size: 48,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 控制区域
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
// 室内/室外切换
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => isFloatingIndoor = true),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isFloatingIndoor
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'室内',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isFloatingIndoor
|
||||
? Colors.white
|
||||
: const Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => isFloatingIndoor = false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: !isFloatingIndoor
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'室外',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: !isFloatingIndoor
|
||||
? Colors.white
|
||||
: const Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 关闭按钮
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => showFloatingMonitor = false),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48, height: 48,
|
||||
decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969))),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -274,15 +720,25 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF86909C))),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(':', style: TextStyle(fontSize: 12, color: Color(0xFFC0C4CC))),
|
||||
const Text(
|
||||
':',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFFC0C4CC)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF1D2129), fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
@@ -294,25 +750,54 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
Widget _buildPositionStateRow(String label, PositionState? positionState) {
|
||||
String value = '未知';
|
||||
if (positionState != null) {
|
||||
value = 'GPS:${positionState.gpsNumber} RTX:${positionState.rtkNumber} 固定:${positionState.isFixed}';
|
||||
value =
|
||||
'GPS:${positionState.gpsNumber} RTX:${positionState.rtkNumber} 固定:${positionState.isFixed}';
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||
),
|
||||
const Spacer(),
|
||||
Flexible(child: Text(value, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129), fontWeight: FontWeight.w500), textAlign: TextAlign.right)),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraListRow(String label, List<CameraInfo> cameras) {
|
||||
String value = cameras.map((c) => '${c.cameraIndex}:${c.cameraPosition}').join(' | ');
|
||||
String value = cameras
|
||||
.map((c) => '${c.cameraIndex}:${c.cameraPosition}')
|
||||
.join(' | ');
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||
),
|
||||
const Spacer(),
|
||||
Flexible(child: Text(value, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129), fontWeight: FontWeight.w500), textAlign: TextAlign.right)),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,22 +49,23 @@ class RobotControlPage extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
RobotStatusBar(robot: robot),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
RobotControlPanel(robot: robot),
|
||||
const SizedBox(height: 8),
|
||||
RobotTaskInfo(robot: robot),
|
||||
],
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RobotControlPanel(robot: robot),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: RobotChecklist(robot: robot)),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: RobotChecklist(robot: robot),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildPathPlanningCard(context),
|
||||
const SizedBox(height: 8),
|
||||
RobotTaskInfo(robot: robot),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -76,4 +77,73 @@ class RobotControlPage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPathPlanningCard(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// TODO: 跳转到路径规划页面
|
||||
debugPrint('点击路径规划');
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.route,
|
||||
color: Color(0xFF165DFF),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'路径规划',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'查看和编辑机器人巡检路径',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../../devices/domain/entities/device_entity.dart';
|
||||
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../data/models/robot_data_model.dart';
|
||||
import '../bloc/robot_list_bloc.dart';
|
||||
import '../bloc/robot_list_event.dart';
|
||||
import '../bloc/robot_list_state.dart';
|
||||
import '../widgets/robot_item_card.dart';
|
||||
import 'robot_control_page.dart';
|
||||
import 'cleaning_weeding_robot_task_page.dart';
|
||||
|
||||
/// 机器人列表页面
|
||||
class RobotListPage extends StatefulWidget {
|
||||
class RobotListPage extends StatelessWidget {
|
||||
const RobotListPage({super.key});
|
||||
|
||||
@override
|
||||
State<RobotListPage> createState() => _RobotListPageState();
|
||||
Widget build(BuildContext context) {
|
||||
final selectedSite = sl<SiteCubit>().state.selectedSite;
|
||||
final siteId = selectedSite?.id;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => sl<RobotListBloc>()..add(RobotListLoadData(siteId: siteId)),
|
||||
child: RobotListView(siteId: siteId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RobotListPageState extends State<RobotListPage> {
|
||||
/// 机器人列表视图
|
||||
class RobotListView extends StatefulWidget {
|
||||
final int? siteId;
|
||||
|
||||
const RobotListView({super.key, required this.siteId});
|
||||
|
||||
@override
|
||||
State<RobotListView> createState() => _RobotListViewState();
|
||||
}
|
||||
|
||||
class _RobotListViewState extends State<RobotListView> {
|
||||
String? _selectedType; // 选中的类型
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
children: [
|
||||
_buildStatsCard(),
|
||||
_buildQuickActions(),
|
||||
_buildCurrentTask(),
|
||||
..._buildRobotList(),
|
||||
],
|
||||
return BlocBuilder<RobotListBloc, RobotListState>(
|
||||
builder: (context, state) {
|
||||
if (state is RobotListLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is RobotListError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载失败: ${state.message}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is RobotListLoaded) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<RobotListBloc>().add(const RobotListRefresh());
|
||||
},
|
||||
color: const Color(0xFF165DFF),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
children: [
|
||||
_buildStatsCard(state),
|
||||
_buildQuickActions(state),
|
||||
_buildCurrentTask(state),
|
||||
..._buildRobotList(state),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsCard() {
|
||||
Widget _buildStatsCard(RobotListLoaded state) {
|
||||
// 计算各类型机器人数量
|
||||
final allRobots = state.robots;
|
||||
final inspectionRobots = allRobots.where((r) => r.type.contains('巡检')).toList();
|
||||
final cleaningRobots = allRobots.where((r) => r.type.contains('清洗')).toList();
|
||||
final weedingRobots = allRobots.where((r) => r.type.contains('除草')).toList();
|
||||
|
||||
final onlineInspection = inspectionRobots.where((r) => r.status == '在线').length;
|
||||
final onlineCleaning = cleaningRobots.where((r) => r.status == '在线').length;
|
||||
final onlineWeeding = weedingRobots.where((r) => r.status == '在线').length;
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
@@ -46,9 +127,16 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedType = _selectedType == '巡检机器人' ? null : '巡检机器人';
|
||||
}),
|
||||
onTap: () {
|
||||
context.read<RobotListBloc>().add(
|
||||
RobotListChangeType(
|
||||
_selectedType == '巡检机器人' ? null : '巡检机器人',
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_selectedType = _selectedType == '巡检机器人' ? null : '巡检机器人';
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -84,12 +172,15 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
children: [
|
||||
Image.asset('assets/images/xunjian.png', width: 48, height: 48),
|
||||
const SizedBox(width: 8),
|
||||
const Text('12', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF))),
|
||||
Text(
|
||||
'${inspectionRobots.length}',
|
||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'在线9台',
|
||||
'在线$onlineInspection台',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -105,9 +196,16 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedType = _selectedType == '清洗机器人' ? null : '清洗机器人';
|
||||
}),
|
||||
onTap: () {
|
||||
context.read<RobotListBloc>().add(
|
||||
RobotListChangeType(
|
||||
_selectedType == '清洗机器人' ? null : '清洗机器人',
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_selectedType = _selectedType == '清洗机器人' ? null : '清洗机器人';
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -143,12 +241,15 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
children: [
|
||||
Image.asset('assets/images/qinxi.png', width: 48, height: 48),
|
||||
const SizedBox(width: 8),
|
||||
const Text('8', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF))),
|
||||
Text(
|
||||
'${cleaningRobots.length}',
|
||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'在线6台',
|
||||
'在线$onlineCleaning台',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -164,9 +265,16 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedType = _selectedType == '除草机器人' ? null : '除草机器人';
|
||||
}),
|
||||
onTap: () {
|
||||
context.read<RobotListBloc>().add(
|
||||
RobotListChangeType(
|
||||
_selectedType == '除草机器人' ? null : '除草机器人',
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_selectedType = _selectedType == '除草机器人' ? null : '除草机器人';
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -203,7 +311,7 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
Image.asset('assets/images/chucao.png', width: 48, height: 48),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'6',
|
||||
'${weedingRobots.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -216,7 +324,7 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'在线4台',
|
||||
'在线$onlineWeeding台',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -234,7 +342,12 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions() {
|
||||
Widget _buildQuickActions(RobotListLoaded state) {
|
||||
// 计算统计数据
|
||||
final onlineCount = state.robots.where((r) => r.status == '在线').length;
|
||||
final workingCount = state.robots.where((r) => r.task != '待机中' && r.task != '充电中').length;
|
||||
final standbyCount = state.robots.where((r) => r.task == '待机中').length;
|
||||
final faultCount = state.robots.where((r) => r.status == '异常' || r.status == '离线').length;
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
@@ -245,13 +358,13 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusItem(value: '19', label: '在线设备', valueColor: const Color(0xFF165DFF)),
|
||||
_buildStatusItem(value: '$onlineCount', label: '在线设备', valueColor: const Color(0xFF165DFF)),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(value: '8', label: '作业中', valueColor: const Color(0xFF00B42A)),
|
||||
_buildStatusItem(value: '$workingCount', label: '作业中', valueColor: const Color(0xFF00B42A)),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(value: '12', label: '待命', valueColor: const Color(0xFFFF7D00)),
|
||||
_buildStatusItem(value: '$standbyCount', label: '待命', valueColor: const Color(0xFFFF7D00)),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(value: '2', label: '故障数', valueColor: const Color(0xFFF53F3F)),
|
||||
_buildStatusItem(value: '$faultCount', label: '故障数', valueColor: const Color(0xFFF53F3F)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -269,7 +382,19 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentTask() {
|
||||
Widget _buildCurrentTask(RobotListLoaded state) {
|
||||
// 获取第一个正在执行任务的机器人
|
||||
final workingRobot = state.robots.firstWhere(
|
||||
(r) => r.task != '待机中' && r.task != '充电中',
|
||||
orElse: () => state.robots.isNotEmpty ? state.robots.first : RobotDataModel(
|
||||
name: '',
|
||||
id: '',
|
||||
type: '',
|
||||
status: '',
|
||||
battery: 0,
|
||||
task: '无任务',
|
||||
),
|
||||
);
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -305,7 +430,7 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildTaskInfo('机器人:', '巡检机器人-01'),
|
||||
_buildTaskInfo('机器人:', workingRobot.name.isNotEmpty ? workingRobot.name : '无'),
|
||||
_buildTaskInfo('区域:', '逆变器区A区'),
|
||||
_buildTaskInfo('进度:', '62%'),
|
||||
_buildTaskInfo('预计完成:', '12:30'),
|
||||
@@ -333,47 +458,55 @@ class _RobotListPageState extends State<RobotListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildRobotList() {
|
||||
final allRobots = [
|
||||
{'name': '巡检机器人-01', 'id': 'RB001', 'type': '巡检机器人', 'status': '在线', 'battery': 85.0, 'task': '逆变器区域巡检'},
|
||||
{'name': '巡检机器人-02', 'id': 'RB002', 'type': '巡检机器人', 'status': '在线', 'battery': 62.0, 'task': '组件区域巡检'},
|
||||
{'name': '巡检机器人-03', 'id': 'RB003', 'type': '巡检机器人', 'status': '离线', 'battery': 15.0, 'task': '充电中'},
|
||||
{'name': '清洗机器人-01', 'id': 'RB004', 'type': '清洗机器人', 'status': '在线', 'battery': 78.0, 'task': '组件清洗作业'},
|
||||
{'name': '清洗机器人-02', 'id': 'RB005', 'type': '清洗机器人', 'status': '在线', 'battery': 91.0, 'task': '待机中'},
|
||||
{'name': '除草机器人-01', 'id': 'RB006', 'type': '除草机器人', 'status': '在线', 'battery': 55.0, 'task': '光伏板除草'},
|
||||
];
|
||||
List<Widget> _buildRobotList(RobotListLoaded state) {
|
||||
final allRobots = state.robots;
|
||||
|
||||
final filteredRobots = _selectedType == null ? allRobots : allRobots.where((robot) => robot['type'] == _selectedType).toList();
|
||||
// 根据选中的类型过滤
|
||||
final filteredRobots = state.selectedType == null
|
||||
? allRobots
|
||||
: allRobots.where((robot) => robot.type.contains(state.selectedType!)).toList();
|
||||
|
||||
if (filteredRobots.isEmpty) {
|
||||
return [const Center(child: Padding(padding: EdgeInsets.all(32), child: Text('暂无机器人数据', style: TextStyle(fontSize: 14, color: Color(0xFF86909C)))))];
|
||||
}
|
||||
|
||||
return filteredRobots.map((robot) => RobotItemCard(
|
||||
name: robot['name'] as String,
|
||||
id: robot['id'] as String,
|
||||
type: robot['type'] as String,
|
||||
status: robot['status'] as String,
|
||||
battery: robot['battery'] as double,
|
||||
task: robot['task'] as String,
|
||||
name: robot.name,
|
||||
id: robot.id,
|
||||
type: robot.type,
|
||||
status: robot.status,
|
||||
battery: robot.battery,
|
||||
task: robot.task,
|
||||
onTap: () {
|
||||
// 巡检机器人使用原来的控制页面
|
||||
// 清洗/除草机器人使用新的任务详情页面
|
||||
if (robot['type'] == '巡检机器人') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RobotControlPage(robot: robot),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CleaningWeedingRobotTaskPage(robot: robot),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 1. 将当前机器人设置为全局选中设备
|
||||
final device = DeviceEntity(
|
||||
deviceName: robot.id,
|
||||
productId: -1,
|
||||
productName: robot.type,
|
||||
tenantId: 0,
|
||||
tenantName: '',
|
||||
status: robot.status == '在线' ? 1 : 0,
|
||||
onlineStatus: robot.status == '在线' ? 1 : 0,
|
||||
);
|
||||
|
||||
context.read<DevicesCubit>().selectDevice(device);
|
||||
|
||||
// 2. 跳转到机器人控制页面
|
||||
final robotMap = {
|
||||
'name': robot.name,
|
||||
'id': robot.id,
|
||||
'type': robot.type,
|
||||
'status': robot.status,
|
||||
'battery': robot.battery,
|
||||
'task': robot.task,
|
||||
};
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RobotControlPage(robot: robotMap),
|
||||
),
|
||||
);
|
||||
},
|
||||
)).toList();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class RobotChecklist extends StatelessWidget {
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 6),
|
||||
_buildCheckItem('逆变器', true),
|
||||
_buildCheckItem('汇流箱', true),
|
||||
_buildCheckItem('支架结构', true),
|
||||
@@ -45,7 +45,7 @@ class RobotChecklist extends StatelessWidget {
|
||||
|
||||
Widget _buildCheckItem(String label, bool isDone, {bool isWarning = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../../core/di/injection.dart';
|
||||
import '../../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../../devices/domain/entities/device_entity.dart';
|
||||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
import '../../../../remote_control/presentation/pages/remote_control_page.dart';
|
||||
|
||||
/// 机器人控制面板
|
||||
class RobotControlPanel extends StatelessWidget {
|
||||
@@ -8,7 +14,46 @@ class RobotControlPanel extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint('👆 控制面板被点击');
|
||||
debugPrint('📋 robot 数据: $robot');
|
||||
|
||||
// 1. 将当前机器人设置为全局选中设备
|
||||
final device = DeviceEntity(
|
||||
deviceName: robot['name']?.toString() ?? '',
|
||||
productId: int.tryParse(robot['id']?.toString() ?? '-1') ?? -1,
|
||||
productName: '割草机产品MC700',
|
||||
tenantId: 0,
|
||||
tenantName: '',
|
||||
status: robot['status'] == '在线' ? 1 : 0,
|
||||
onlineStatus: robot['status'] == '在线' ? 1 : 0,
|
||||
);
|
||||
|
||||
debugPrint('✅ 设置选中设备: ${device.deviceName}');
|
||||
context.read<DevicesCubit>().selectDevice(device);
|
||||
|
||||
// 打印当前选中的设备信息
|
||||
final selectedDevice = context.read<DevicesCubit>().state.selectedDevice;
|
||||
debugPrint('📱 当前选中设备信息:');
|
||||
debugPrint(' - deviceName: ${selectedDevice?.deviceName}');
|
||||
debugPrint(' - productName: ${selectedDevice?.productName}');
|
||||
debugPrint(' - status: ${selectedDevice?.status}');
|
||||
debugPrint(' - onlineStatus: ${selectedDevice?.onlineStatus}');
|
||||
|
||||
// 2. 跳转到远程遥控页面
|
||||
debugPrint('🚀 准备跳转...');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (_) => sl<RemoteControlCubit>()..startControlLoop(),
|
||||
child: const RemoteControlPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@@ -124,6 +169,7 @@ class RobotControlPanel extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,31 +26,30 @@ class RobotHeaderCard extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
robot['name'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
robot['name'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'ID: ${robot['id']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'在线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 信号强度
|
||||
Row(
|
||||
children: [
|
||||
@@ -71,6 +70,23 @@ class RobotHeaderCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 在线状态
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'在线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 设置图标
|
||||
const Icon(Icons.settings, size: 20, color: Color(0xFF86909C)),
|
||||
],
|
||||
|
||||
@@ -72,10 +72,12 @@ class RobotItemCard extends StatelessWidget {
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../home/presentation/pages/running_status_page.dart';
|
||||
|
||||
/// 机器人状态栏
|
||||
class RobotStatusBar extends StatelessWidget {
|
||||
@@ -8,47 +9,69 @@ class RobotStatusBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const RunningStatusPage(),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusItem(
|
||||
label: '速度',
|
||||
value: '1.2',
|
||||
unit: 'm/s',
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '里程',
|
||||
value: '2.36',
|
||||
unit: 'km',
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '电量',
|
||||
value: '82',
|
||||
unit: '%',
|
||||
valueColor: const Color(0xFF00B42A),
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '信号',
|
||||
value: '强',
|
||||
unit: '',
|
||||
valueColor: const Color(0xFF00B42A),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusItem(
|
||||
label: '速度',
|
||||
value: '1.2',
|
||||
unit: 'm/s',
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '里程',
|
||||
value: '2.36',
|
||||
unit: 'km',
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '电量',
|
||||
value: '82',
|
||||
unit: '%',
|
||||
valueColor: const Color(0xFF00B42A),
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '信号',
|
||||
value: '强',
|
||||
unit: '',
|
||||
valueColor: const Color(0xFF00B42A),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
|
||||
@@ -101,8 +102,7 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
|
||||
return AlarmItemCard(
|
||||
alarm: alarm,
|
||||
onTap: () {
|
||||
// TODO: 跳转到告警详情页
|
||||
debugPrint('点击告警: ${alarm.title}');
|
||||
context.push('/alarm_center/detail/${alarm.id}');
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
@@ -213,6 +213,35 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 铃铛图标(消息通知)
|
||||
Stack(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.notifications_none,
|
||||
size: 24,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
onPressed: () {
|
||||
context.push('/message_center');
|
||||
},
|
||||
),
|
||||
// 未读消息红点
|
||||
Positioned(
|
||||
right: 10,
|
||||
top: 10,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF53F3F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 筛选图标
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.filter_list,
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/message_center/presentation/pages/message_center_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_detail_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_center_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_detail_page.dart';
|
||||
|
||||
/// 告警中心路由配置
|
||||
class AlarmCenterRoutes {
|
||||
static List<RouteBase> get routes => [
|
||||
GoRoute(
|
||||
path: 'alarm_center',
|
||||
path: '/alarm_center',
|
||||
name: 'alarmCenter',
|
||||
builder: (context, state) => const AlarmCenterPage(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'detail/:alarmId',
|
||||
name: 'alarmDetail',
|
||||
builder: (context, state) {
|
||||
final alarmId = state.pathParameters['alarmId']!;
|
||||
return BlocProvider(
|
||||
create: (_) => sl<AlarmDetailCubit>(),
|
||||
child: AlarmDetailPage(alarmId: alarmId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// 消息中心页路由
|
||||
GoRoute(
|
||||
path: '/message_center',
|
||||
name: 'messageCenter',
|
||||
builder: (context, state) => const MessageCenterPage(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
126
lib/main.dart
126
lib/main.dart
@@ -79,8 +79,12 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<DeviceStatusBloc>.value(value: deviceStatusBloc),
|
||||
BlocProvider<LocaleCubit>.value(value: localeCubit),
|
||||
BlocProvider<TabConfigCubit>.value(value: sl<TabConfigCubit>()),
|
||||
BlocProvider<PermissionRequestBloc>(create: (_) => sl<PermissionRequestBloc>()),
|
||||
BlocProvider<UpdateCubit>(create: (_) => UpdateCubit(VersionCheckService())),
|
||||
BlocProvider<PermissionRequestBloc>(
|
||||
create: (_) => sl<PermissionRequestBloc>(),
|
||||
),
|
||||
BlocProvider<UpdateCubit>(
|
||||
create: (_) => UpdateCubit(VersionCheckService()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<LocaleCubit, Locale>(
|
||||
bloc: localeCubit,
|
||||
@@ -114,7 +118,8 @@ class _LifecycleListener extends StatefulWidget {
|
||||
State<_LifecycleListener> createState() => _LifecycleListenerState();
|
||||
}
|
||||
|
||||
class _LifecycleListenerState extends State<_LifecycleListener> with WidgetsBindingObserver {
|
||||
class _LifecycleListenerState extends State<_LifecycleListener>
|
||||
with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -172,7 +177,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
listener: (context, state) {
|
||||
if (state is UpdateFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('更新失败: ${state.error}'), backgroundColor: Colors.red),
|
||||
SnackBar(
|
||||
content: Text('更新失败: ${state.error}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -188,23 +196,40 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
child: Center(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 30),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('🎉 发现新版本',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'🎉 发现新版本',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('版本号: ${state.versionInfo.version}',
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
Text('更新类型: ${state.versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
Text(
|
||||
'版本号: ${state.versionInfo.version}',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
'更新类型: ${state.versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
if (state.versionInfo.updateDesc.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'更新内容:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(state.versionInfo.updateDesc),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
@@ -215,20 +240,28 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
border: Border.all(
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'💡 选择下载方式',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'• 自动安装:App 内下载并调起系统安装\n'
|
||||
'• 手动下载:在浏览器中下载安装',
|
||||
style: TextStyle(fontSize: 13, height: 1.5),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -240,7 +273,9 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
children: [
|
||||
if (!state.versionInfo.forceUpdate)
|
||||
TextButton(
|
||||
onPressed: () => context.read<UpdateCubit>().cancelUpdate(),
|
||||
onPressed: () => context
|
||||
.read<UpdateCubit>()
|
||||
.cancelUpdate(),
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -266,7 +301,11 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
onPressed: () {
|
||||
final info = state.versionInfo;
|
||||
if (info.apkUrl != null) {
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
||||
context
|
||||
.read<UpdateCubit>()
|
||||
.downloadAndInstallApk(
|
||||
info.apkUrl!,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('手动下载'),
|
||||
@@ -276,7 +315,11 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
onPressed: () {
|
||||
final info = state.versionInfo;
|
||||
if (info.apkUrl != null) {
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
||||
context
|
||||
.read<UpdateCubit>()
|
||||
.downloadAndInstallApk(
|
||||
info.apkUrl!,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('自动安装'),
|
||||
@@ -307,7 +350,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
children: [
|
||||
const Text(
|
||||
'📱 整包更新步骤',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStepItem(1, '在浏览器中下载 APK', true),
|
||||
@@ -347,7 +393,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
children: [
|
||||
Text(
|
||||
state.isPatch ? '⬇️ 正在下载补丁...' : '⬇️ 正在下载 APK...',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
@@ -362,7 +411,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
),
|
||||
Text(
|
||||
'${(state.progress * 100).toInt()}%',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -370,7 +422,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'请稍候...',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -392,10 +447,19 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.check_circle, color: Colors.green, size: 60),
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 60,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('✅ 更新已完成',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const Text(
|
||||
'✅ 更新已完成',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'补丁已成功应用!\n\n请完全关闭 App 后重新打开,\n即可体验新版本功能。',
|
||||
@@ -435,8 +499,13 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('状态: ${state.runtimeType.toString().replaceAll('Update', '')}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||
Text(
|
||||
'状态: ${state.runtimeType.toString().replaceAll('Update', '')}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
// 🔥 暂时关闭刷新按钮和点击阴影
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
@@ -485,8 +554,7 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (completed)
|
||||
const Icon(Icons.check, color: Colors.green, size: 20),
|
||||
if (completed) const Icon(Icons.check, color: Colors.green, size: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter_svg: ^2.2.3
|
||||
vibration: ^3.1.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -93,6 +93,9 @@ dependencies:
|
||||
image_picker: ^1.1.2
|
||||
video_player: ^2.8.2
|
||||
|
||||
# ===== 火山引擎 RTC 实时音视频 =====
|
||||
#volc_engine_rtc: ^3.60.4
|
||||
|
||||
|
||||
# ===== 屏幕适配,高刷等 =====
|
||||
# flutter_displaymode: ^0.7.0
|
||||
|
||||
Reference in New Issue
Block a user