机器人的列表接口

This commit is contained in:
2026-06-01 17:33:16 +08:00
parent 9154174614
commit 93e69a5e4b
23 changed files with 1312 additions and 269 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ dependencies:
sdk: flutter
flutter_svg: ^2.2.3
vibration: ^3.1.8
dev_dependencies:
flutter_test:

View File

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