机器人的列表接口
This commit is contained in:
@@ -19,16 +19,21 @@ class HttpApiConsts {
|
|||||||
// 获取光伏电站列表
|
// 获取光伏电站列表
|
||||||
static const String getSiteList = "$baseUrl/system/site/list";
|
static const String getSiteList = "$baseUrl/system/site/list";
|
||||||
|
|
||||||
|
// 获取场站下的设备列表
|
||||||
|
static const String getSiteDeviceList = "$baseUrl/iot/device/getSiteList";
|
||||||
|
|
||||||
// 获取场站下的无人机机场列表
|
// 获取场站下的无人机机场列表
|
||||||
static const String getSiteUAVList = "$baseUrl/iot/UAV/getSiteUAVList";
|
static const String getSiteUAVList = "$baseUrl/iot/UAV/getSiteUAVList";
|
||||||
|
|
||||||
// 获取UAV状态详情
|
// 获取UAV状态详情
|
||||||
static const String getUAVState = "$baseUrl/iot/UAV/getUAVState";
|
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 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:go_router/go_router.dart';
|
||||||
import 'package:maibu_satabot_v2/core/router/route_paths.dart';
|
import 'package:maibu_satabot_v2/core/router/route_paths.dart';
|
||||||
import 'package:maibu_satabot_v2/features/ai/presentation/routes/ai_routes.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';
|
import 'go_router_refresh_stream.dart';
|
||||||
|
|
||||||
GoRouter createRouter(AuthCubit authCubit) {
|
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(
|
return GoRouter(
|
||||||
initialLocation: RoutePaths.login,
|
initialLocation: RoutePaths.login,
|
||||||
refreshListenable: GoRouterRefreshStream(authCubit.stream),
|
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_entity.dart';
|
||||||
import '../../domain/entities/device_status_entity.dart';
|
import '../../domain/entities/device_status_entity.dart';
|
||||||
import '../models/device_data_model.dart';
|
import '../models/device_data_model.dart';
|
||||||
@@ -6,17 +8,22 @@ import '../models/device_status_data_model.dart';
|
|||||||
/// 设备远程数据源抽象接口
|
/// 设备远程数据源抽象接口
|
||||||
abstract class DeviceRemoteDataSource {
|
abstract class DeviceRemoteDataSource {
|
||||||
Future<DeviceStatusDataModel> getDeviceStatus();
|
Future<DeviceStatusDataModel> getDeviceStatus();
|
||||||
Future<List<DeviceDataModel>> getDeviceList({String? typeFilter});
|
Future<List<DeviceDataModel>> getDeviceList({
|
||||||
|
int? siteId,
|
||||||
|
String? typeFilter,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设备远程数据源实现类 - 模拟从 API 获取数据
|
/// 设备远程数据源实现类 - 从真实 API 获取数据
|
||||||
class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
DeviceRemoteDataSourceImpl(this.dio);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DeviceStatusDataModel> getDeviceStatus() async {
|
Future<DeviceStatusDataModel> getDeviceStatus() async {
|
||||||
// 模拟网络延迟
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
// 模拟数据
|
|
||||||
return const DeviceStatusDataModel(
|
return const DeviceStatusDataModel(
|
||||||
total: 136,
|
total: 136,
|
||||||
online: 122,
|
online: 122,
|
||||||
@@ -26,11 +33,119 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<DeviceDataModel>> getDeviceList({String? typeFilter}) async {
|
Future<List<DeviceDataModel>> getDeviceList({
|
||||||
// 模拟网络延迟
|
int? siteId,
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
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 = [
|
final allDevices = [
|
||||||
const DeviceDataModel(
|
const DeviceDataModel(
|
||||||
deviceId: '10001',
|
deviceId: '10001',
|
||||||
@@ -85,7 +200,6 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 根据类型过滤
|
|
||||||
if (typeFilter != null && typeFilter != '全部') {
|
if (typeFilter != null && typeFilter != '全部') {
|
||||||
return allDevices.where((device) => device.type == typeFilter).toList();
|
return allDevices.where((device) => device.type == typeFilter).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ class DeviceRepositoryImpl implements DeviceRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<DeviceEntity>> getDeviceList({String? typeFilter}) async {
|
Future<List<DeviceEntity>> getDeviceList({int? siteId, String? typeFilter}) async {
|
||||||
final dataModels = await remoteDataSource.getDeviceList(typeFilter: typeFilter);
|
final dataModels = await remoteDataSource.getDeviceList(siteId: siteId, typeFilter: typeFilter);
|
||||||
return dataModels.map((model) => model.toEntity()).toList();
|
return dataModels.map((model) => model.toEntity()).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import '../entities/device_status_entity.dart';
|
|||||||
/// 设备仓储抽象接口
|
/// 设备仓储抽象接口
|
||||||
abstract class DeviceRepository {
|
abstract class DeviceRepository {
|
||||||
Future<DeviceStatusEntity> getDeviceStatus();
|
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});
|
const GetDeviceStatusDataUseCase({required this.repository});
|
||||||
|
|
||||||
Future<DeviceStatusResponse> execute({String? typeFilter}) async {
|
Future<DeviceStatusResponse> execute({int? siteId, String? typeFilter}) async {
|
||||||
try {
|
try {
|
||||||
final status = await repository.getDeviceStatus();
|
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);
|
return DeviceStatusResponse(status: status, devices: devices);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('获取设备数据失败: ${e.toString()}');
|
throw Exception('获取设备数据失败: ${e.toString()}');
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
|||||||
emit(const DeviceStatusLoading());
|
emit(const DeviceStatusLoading());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await getDeviceStatusDataUseCase.execute();
|
final response = await getDeviceStatusDataUseCase.execute(
|
||||||
|
siteId: event.siteId,
|
||||||
|
);
|
||||||
|
|
||||||
emit(DeviceStatusLoaded(
|
emit(DeviceStatusLoaded(
|
||||||
deviceStatus: response.status,
|
deviceStatus: response.status,
|
||||||
devices: response.devices,
|
devices: response.devices,
|
||||||
|
siteId: event.siteId,
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(DeviceStatusError(e.toString()));
|
emit(DeviceStatusError(e.toString()));
|
||||||
@@ -41,6 +44,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await getDeviceStatusDataUseCase.execute(
|
final response = await getDeviceStatusDataUseCase.execute(
|
||||||
|
siteId: currentState.siteId,
|
||||||
typeFilter: currentState.selectedType == '全部'
|
typeFilter: currentState.selectedType == '全部'
|
||||||
? null
|
? null
|
||||||
: currentState.selectedType,
|
: currentState.selectedType,
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ abstract class DeviceStatusEvent extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DeviceStatusLoadData extends DeviceStatusEvent {
|
class DeviceStatusLoadData extends DeviceStatusEvent {
|
||||||
const DeviceStatusLoadData();
|
final int? siteId;
|
||||||
|
|
||||||
|
const DeviceStatusLoadData({this.siteId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [siteId];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeviceStatusRefresh extends DeviceStatusEvent {
|
class DeviceStatusRefresh extends DeviceStatusEvent {
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ class DeviceStatusLoaded extends DeviceStatusState {
|
|||||||
final List<DeviceEntity> devices;
|
final List<DeviceEntity> devices;
|
||||||
final String selectedType;
|
final String selectedType;
|
||||||
final String searchKeyword;
|
final String searchKeyword;
|
||||||
|
final int? siteId;
|
||||||
|
|
||||||
const DeviceStatusLoaded({
|
const DeviceStatusLoaded({
|
||||||
required this.deviceStatus,
|
required this.deviceStatus,
|
||||||
required this.devices,
|
required this.devices,
|
||||||
this.selectedType = '全部',
|
this.selectedType = '全部',
|
||||||
this.searchKeyword = '',
|
this.searchKeyword = '',
|
||||||
|
this.siteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
DeviceStatusLoaded copyWith({
|
DeviceStatusLoaded copyWith({
|
||||||
@@ -35,17 +37,19 @@ class DeviceStatusLoaded extends DeviceStatusState {
|
|||||||
List<DeviceEntity>? devices,
|
List<DeviceEntity>? devices,
|
||||||
String? selectedType,
|
String? selectedType,
|
||||||
String? searchKeyword,
|
String? searchKeyword,
|
||||||
|
int? siteId,
|
||||||
}) {
|
}) {
|
||||||
return DeviceStatusLoaded(
|
return DeviceStatusLoaded(
|
||||||
deviceStatus: deviceStatus ?? this.deviceStatus,
|
deviceStatus: deviceStatus ?? this.deviceStatus,
|
||||||
devices: devices ?? this.devices,
|
devices: devices ?? this.devices,
|
||||||
selectedType: selectedType ?? this.selectedType,
|
selectedType: selectedType ?? this.selectedType,
|
||||||
searchKeyword: searchKeyword ?? this.searchKeyword,
|
searchKeyword: searchKeyword ?? this.searchKeyword,
|
||||||
|
siteId: siteId ?? this.siteId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [deviceStatus, devices, selectedType, searchKeyword];
|
List<Object?> get props => [deviceStatus, devices, selectedType, searchKeyword, siteId];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeviceStatusError extends DeviceStatusState {
|
class DeviceStatusError extends DeviceStatusState {
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ class DeviceStatusPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final selectedSite = sl<SiteCubit>().state.selectedSite;
|
||||||
|
final siteId = selectedSite?.id;
|
||||||
|
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (_) => sl<DeviceStatusBloc>()..add(const DeviceStatusLoadData()),
|
create: (_) => sl<DeviceStatusBloc>()..add(DeviceStatusLoadData(siteId: siteId)),
|
||||||
child: const DeviceStatusView(),
|
child: const DeviceStatusView(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import 'drone_monitor_page.dart';
|
|||||||
class DroneStationDetailPage extends StatefulWidget {
|
class DroneStationDetailPage extends StatefulWidget {
|
||||||
final DroneStationEntity station;
|
final DroneStationEntity station;
|
||||||
|
|
||||||
const DroneStationDetailPage({
|
const DroneStationDetailPage({super.key, required this.station});
|
||||||
super.key,
|
|
||||||
required this.station,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DroneStationDetailPage> createState() => _DroneStationDetailPageState();
|
State<DroneStationDetailPage> createState() => _DroneStationDetailPageState();
|
||||||
@@ -24,14 +21,21 @@ class DroneStationDetailPage extends StatefulWidget {
|
|||||||
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||||
late DroneStationBloc _bloc;
|
late DroneStationBloc _bloc;
|
||||||
|
|
||||||
|
// 悬浮视频监控状态
|
||||||
|
bool showFloatingMonitor = false;
|
||||||
|
bool isFloatingIndoor = true;
|
||||||
|
Offset floatingPosition = const Offset(20, 200);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_bloc = sl<DroneStationBloc>();
|
_bloc = sl<DroneStationBloc>();
|
||||||
_bloc.add(UAVDetailLoad(
|
_bloc.add(
|
||||||
|
UAVDetailLoad(
|
||||||
gatewaySn: widget.station.gatewaySn,
|
gatewaySn: widget.station.gatewaySn,
|
||||||
deviceSn: widget.station.deviceSn,
|
deviceSn: widget.station.deviceSn,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -76,18 +80,32 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
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),
|
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),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_bloc.add(UAVDetailLoad(
|
_bloc.add(
|
||||||
|
UAVDetailLoad(
|
||||||
gatewaySn: widget.station.gatewaySn,
|
gatewaySn: widget.station.gatewaySn,
|
||||||
deviceSn: widget.station.deviceSn,
|
deviceSn: widget.station.deviceSn,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF165DFF)),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF165DFF),
|
||||||
|
),
|
||||||
child: const Text('重试'),
|
child: const Text('重试'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -107,16 +125,78 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(UAVDetailEntity detail) {
|
Widget _buildContent(UAVDetailEntity detail) {
|
||||||
return ListView(
|
return Stack(
|
||||||
|
children: [
|
||||||
|
ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
_buildAirportStatusCard(detail),
|
_buildAirportStatusCard(detail),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
_buildMonitorCard(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
_buildDroneStatusCard(detail),
|
_buildDroneStatusCard(detail),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildQuickActions(),
|
_buildQuickActions(),
|
||||||
const SizedBox(height: 20),
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +207,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2)),
|
BoxShadow(
|
||||||
|
color: Color(0x0D000000),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -135,28 +219,67 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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(),
|
const Spacer(),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
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),
|
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 SizedBox(height: 16),
|
||||||
const Divider(height: 1, color: Color(0xFFF2F3F5)),
|
const Divider(height: 1, color: Color(0xFFF2F3F5)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildInfoRow('机场名称', detail.callsign.isNotEmpty ? detail.callsign : '未知'),
|
_buildInfoRow(
|
||||||
|
'机场名称',
|
||||||
|
detail.callsign.isNotEmpty ? detail.callsign : '未知',
|
||||||
|
),
|
||||||
_buildInfoRow('设备序列号', detail.deviceSn),
|
_buildInfoRow('设备序列号', detail.deviceSn),
|
||||||
_buildInfoRow('网关序列号', detail.gatewaySn),
|
_buildInfoRow('网关序列号', detail.gatewaySn),
|
||||||
_buildInfoRow('位置坐标', (detail.latitude != null && detail.longitude != null) ? '${detail.latitude}, ${detail.longitude}' : '未知'),
|
_buildInfoRow(
|
||||||
_buildInfoRow('电量', detail.capacityPercent != null ? '${detail.capacityPercent}%' : '未知'),
|
'位置坐标',
|
||||||
_buildInfoRow('环境温度', detail.environmentTemperature != null ? '${detail.environmentTemperature}°C' : '未知'),
|
(detail.latitude != null && detail.longitude != null)
|
||||||
_buildInfoRow('风速', detail.windSpeed != null ? '${detail.windSpeed} m/s' : '未知'),
|
? '${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.rainfall ?? '未知'),
|
||||||
_buildInfoRow('网络状态', detail.networkState?.toString() ?? '未知'),
|
_buildInfoRow('网络状态', detail.networkState?.toString() ?? '未知'),
|
||||||
_buildPositionStateRow('位置状态', detail.positionState),
|
_buildPositionStateRow('位置状态', detail.positionState),
|
||||||
@@ -168,45 +291,95 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
Widget _buildDroneStatusCard(UAVDetailEntity detail) {
|
Widget _buildDroneStatusCard(UAVDetailEntity detail) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const DroneVideoControlPage()));
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const DroneVideoControlPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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(),
|
const Spacer(),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
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),
|
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),
|
const SizedBox(height: 16),
|
||||||
_buildInfoRow('无人机呼号', detail.droneCallsign.isNotEmpty ? detail.droneCallsign : '未知'),
|
_buildInfoRow(
|
||||||
|
'无人机呼号',
|
||||||
|
detail.droneCallsign.isNotEmpty ? detail.droneCallsign : '未知',
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoRow('设备序列号', detail.deviceSn),
|
_buildInfoRow('设备序列号', detail.deviceSn),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoRow('电量', detail.capacityPercent != null ? '${detail.capacityPercent}%' : '未知'),
|
_buildInfoRow(
|
||||||
|
'电量',
|
||||||
|
detail.capacityPercent != null
|
||||||
|
? '${detail.capacityPercent}%'
|
||||||
|
: '未知',
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoRow('高度', detail.height != null ? '${detail.height} m' : '未知'),
|
_buildInfoRow(
|
||||||
|
'高度',
|
||||||
|
detail.height != null ? '${detail.height} m' : '未知',
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoRow('距离home点', detail.homeDistance != null ? '${detail.homeDistance} m' : '未知'),
|
_buildInfoRow(
|
||||||
|
'距离home点',
|
||||||
|
detail.homeDistance != null ? '${detail.homeDistance} m' : '未知',
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInfoRow('实时电量', detail.liveCapacity != null ? '${detail.liveCapacity}%' : '未知'),
|
_buildInfoRow(
|
||||||
if (detail.gatewayCameraList != null && detail.gatewayCameraList!.isNotEmpty) ...[
|
'实时电量',
|
||||||
|
detail.liveCapacity != null ? '${detail.liveCapacity}%' : '未知',
|
||||||
|
),
|
||||||
|
if (detail.gatewayCameraList != null &&
|
||||||
|
detail.gatewayCameraList!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildCameraListRow('网关摄像头', detail.gatewayCameraList!),
|
_buildCameraListRow('网关摄像头', detail.gatewayCameraList!),
|
||||||
],
|
],
|
||||||
@@ -222,22 +395,63 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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),
|
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),
|
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),
|
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() {
|
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(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 48, height: 48,
|
width: 48,
|
||||||
decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
child: Icon(icon, color: color, size: 24),
|
child: Icon(icon, color: color, size: 24),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
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: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 80,
|
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 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),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
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,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -294,25 +750,54 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
Widget _buildPositionStateRow(String label, PositionState? positionState) {
|
Widget _buildPositionStateRow(String label, PositionState? positionState) {
|
||||||
String value = '未知';
|
String value = '未知';
|
||||||
if (positionState != null) {
|
if (positionState != null) {
|
||||||
value = 'GPS:${positionState.gpsNumber} RTX:${positionState.rtkNumber} 固定:${positionState.isFixed}';
|
value =
|
||||||
|
'GPS:${positionState.gpsNumber} RTX:${positionState.rtkNumber} 固定:${positionState.isFixed}';
|
||||||
}
|
}
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))),
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||||
|
),
|
||||||
const Spacer(),
|
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) {
|
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(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))),
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||||
|
),
|
||||||
const Spacer(),
|
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),
|
const SizedBox(height: 8),
|
||||||
RobotStatusBar(robot: robot),
|
RobotStatusBar(robot: robot),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
IntrinsicHeight(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: RobotControlPanel(robot: robot),
|
||||||
children: [
|
|
||||||
RobotControlPanel(robot: robot),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
RobotTaskInfo(robot: robot),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: RobotChecklist(robot: robot)),
|
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/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 '../widgets/robot_item_card.dart';
|
||||||
import 'robot_control_page.dart';
|
import 'robot_control_page.dart';
|
||||||
import 'cleaning_weeding_robot_task_page.dart';
|
import 'cleaning_weeding_robot_task_page.dart';
|
||||||
|
|
||||||
/// 机器人列表页面
|
/// 机器人列表页面
|
||||||
class RobotListPage extends StatefulWidget {
|
class RobotListPage extends StatelessWidget {
|
||||||
const RobotListPage({super.key});
|
const RobotListPage({super.key});
|
||||||
|
|
||||||
@override
|
@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; // 选中的类型
|
String? _selectedType; // 选中的类型
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return BlocBuilder<RobotListBloc, RobotListState>(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
builder: (context, state) {
|
||||||
children: [
|
if (state is RobotListLoading) {
|
||||||
_buildStatsCard(),
|
return const Center(
|
||||||
_buildQuickActions(),
|
child: CircularProgressIndicator(
|
||||||
_buildCurrentTask(),
|
color: Color(0xFF165DFF),
|
||||||
..._buildRobotList(),
|
),
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatsCard() {
|
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(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(
|
return Container(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
@@ -46,9 +127,16 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => setState(() {
|
onTap: () {
|
||||||
|
context.read<RobotListBloc>().add(
|
||||||
|
RobotListChangeType(
|
||||||
|
_selectedType == '巡检机器人' ? null : '巡检机器人',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
_selectedType = _selectedType == '巡检机器人' ? null : '巡检机器人';
|
_selectedType = _selectedType == '巡检机器人' ? null : '巡检机器人';
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -84,12 +172,15 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
children: [
|
children: [
|
||||||
Image.asset('assets/images/xunjian.png', width: 48, height: 48),
|
Image.asset('assets/images/xunjian.png', width: 48, height: 48),
|
||||||
const SizedBox(width: 8),
|
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),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'在线9台',
|
'在线$onlineInspection台',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -105,9 +196,16 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => setState(() {
|
onTap: () {
|
||||||
|
context.read<RobotListBloc>().add(
|
||||||
|
RobotListChangeType(
|
||||||
|
_selectedType == '清洗机器人' ? null : '清洗机器人',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
_selectedType = _selectedType == '清洗机器人' ? null : '清洗机器人';
|
_selectedType = _selectedType == '清洗机器人' ? null : '清洗机器人';
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -143,12 +241,15 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
children: [
|
children: [
|
||||||
Image.asset('assets/images/qinxi.png', width: 48, height: 48),
|
Image.asset('assets/images/qinxi.png', width: 48, height: 48),
|
||||||
const SizedBox(width: 8),
|
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),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'在线6台',
|
'在线$onlineCleaning台',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -164,9 +265,16 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => setState(() {
|
onTap: () {
|
||||||
|
context.read<RobotListBloc>().add(
|
||||||
|
RobotListChangeType(
|
||||||
|
_selectedType == '除草机器人' ? null : '除草机器人',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
_selectedType = _selectedType == '除草机器人' ? null : '除草机器人';
|
_selectedType = _selectedType == '除草机器人' ? null : '除草机器人';
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -203,7 +311,7 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
Image.asset('assets/images/chucao.png', width: 48, height: 48),
|
Image.asset('assets/images/chucao.png', width: 48, height: 48),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'6',
|
'${weedingRobots.length}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -216,7 +324,7 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'在线4台',
|
'在线$onlineWeeding台',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
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(
|
return Container(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||||
@@ -245,13 +358,13 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
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)),
|
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)),
|
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)),
|
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(
|
return Container(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -305,7 +430,7 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildTaskInfo('机器人:', '巡检机器人-01'),
|
_buildTaskInfo('机器人:', workingRobot.name.isNotEmpty ? workingRobot.name : '无'),
|
||||||
_buildTaskInfo('区域:', '逆变器区A区'),
|
_buildTaskInfo('区域:', '逆变器区A区'),
|
||||||
_buildTaskInfo('进度:', '62%'),
|
_buildTaskInfo('进度:', '62%'),
|
||||||
_buildTaskInfo('预计完成:', '12:30'),
|
_buildTaskInfo('预计完成:', '12:30'),
|
||||||
@@ -333,47 +458,55 @@ class _RobotListPageState extends State<RobotListPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildRobotList() {
|
List<Widget> _buildRobotList(RobotListLoaded state) {
|
||||||
final allRobots = [
|
final allRobots = state.robots;
|
||||||
{'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': '光伏板除草'},
|
|
||||||
];
|
|
||||||
|
|
||||||
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) {
|
if (filteredRobots.isEmpty) {
|
||||||
return [const Center(child: Padding(padding: EdgeInsets.all(32), child: Text('暂无机器人数据', style: TextStyle(fontSize: 14, color: Color(0xFF86909C)))))];
|
return [const Center(child: Padding(padding: EdgeInsets.all(32), child: Text('暂无机器人数据', style: TextStyle(fontSize: 14, color: Color(0xFF86909C)))))];
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredRobots.map((robot) => RobotItemCard(
|
return filteredRobots.map((robot) => RobotItemCard(
|
||||||
name: robot['name'] as String,
|
name: robot.name,
|
||||||
id: robot['id'] as String,
|
id: robot.id,
|
||||||
type: robot['type'] as String,
|
type: robot.type,
|
||||||
status: robot['status'] as String,
|
status: robot.status,
|
||||||
battery: robot['battery'] as double,
|
battery: robot.battery,
|
||||||
task: robot['task'] as String,
|
task: robot.task,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// 巡检机器人使用原来的控制页面
|
// 1. 将当前机器人设置为全局选中设备
|
||||||
// 清洗/除草机器人使用新的任务详情页面
|
final device = DeviceEntity(
|
||||||
if (robot['type'] == '巡检机器人') {
|
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(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => RobotControlPage(robot: robot),
|
builder: (context) => RobotControlPage(robot: robotMap),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => CleaningWeedingRobotTaskPage(robot: robot),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)).toList();
|
)).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class RobotChecklist extends StatelessWidget {
|
|||||||
color: Color(0xFF1D2129),
|
color: Color(0xFF1D2129),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
_buildCheckItem('逆变器', true),
|
_buildCheckItem('逆变器', true),
|
||||||
_buildCheckItem('汇流箱', true),
|
_buildCheckItem('汇流箱', true),
|
||||||
_buildCheckItem('支架结构', true),
|
_buildCheckItem('支架结构', true),
|
||||||
@@ -45,7 +45,7 @@ class RobotChecklist extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildCheckItem(String label, bool isDone, {bool isWarning = false}) {
|
Widget _buildCheckItem(String label, bool isDone, {bool isWarning = false}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class RobotControlPanel extends StatelessWidget {
|
||||||
@@ -8,7 +14,46 @@ class RobotControlPanel extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -124,6 +169,7 @@ class RobotControlPanel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,32 +25,31 @@ class RobotHeaderCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
robot['name'] as String,
|
robot['name'] as String,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF1D2129),
|
color: Color(0xFF1D2129),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 4),
|
||||||
Container(
|
Text(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
'ID: ${robot['id']}',
|
||||||
decoration: BoxDecoration(
|
style: const TextStyle(
|
||||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'在线',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF00B42A),
|
color: Color(0xFF86909C),
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
// 信号强度
|
// 信号强度
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -71,6 +70,23 @@ class RobotHeaderCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
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)),
|
const Icon(Icons.settings, size: 20, color: Color(0xFF86909C)),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -72,10 +72,12 @@ class RobotItemCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
name,
|
name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF1D2129),
|
color: Color(0xFF1D2129),
|
||||||
),
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../../home/presentation/pages/running_status_page.dart';
|
||||||
|
|
||||||
/// 机器人状态栏
|
/// 机器人状态栏
|
||||||
class RobotStatusBar extends StatelessWidget {
|
class RobotStatusBar extends StatelessWidget {
|
||||||
@@ -8,8 +9,17 @@ class RobotStatusBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return GestureDetector(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const RunningStatusPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -21,6 +31,9 @@ class RobotStatusBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_buildStatusItem(
|
_buildStatusItem(
|
||||||
@@ -50,6 +63,16 @@ class RobotStatusBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_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/bloc/alarm_state.dart';
|
||||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.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(
|
return AlarmItemCard(
|
||||||
alarm: alarm,
|
alarm: alarm,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// TODO: 跳转到告警详情页
|
context.push('/alarm_center/detail/${alarm.id}');
|
||||||
debugPrint('点击告警: ${alarm.title}');
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -213,6 +213,35 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
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(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.filter_list,
|
Icons.filter_list,
|
||||||
|
|||||||
@@ -1,13 +1,37 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.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_center_page.dart';
|
||||||
|
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_detail_page.dart';
|
||||||
|
|
||||||
/// 告警中心路由配置
|
/// 告警中心路由配置
|
||||||
class AlarmCenterRoutes {
|
class AlarmCenterRoutes {
|
||||||
static List<RouteBase> get routes => [
|
static List<RouteBase> get routes => [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'alarm_center',
|
path: '/alarm_center',
|
||||||
name: 'alarmCenter',
|
name: 'alarmCenter',
|
||||||
builder: (context, state) => const AlarmCenterPage(),
|
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<DeviceStatusBloc>.value(value: deviceStatusBloc),
|
||||||
BlocProvider<LocaleCubit>.value(value: localeCubit),
|
BlocProvider<LocaleCubit>.value(value: localeCubit),
|
||||||
BlocProvider<TabConfigCubit>.value(value: sl<TabConfigCubit>()),
|
BlocProvider<TabConfigCubit>.value(value: sl<TabConfigCubit>()),
|
||||||
BlocProvider<PermissionRequestBloc>(create: (_) => sl<PermissionRequestBloc>()),
|
BlocProvider<PermissionRequestBloc>(
|
||||||
BlocProvider<UpdateCubit>(create: (_) => UpdateCubit(VersionCheckService())),
|
create: (_) => sl<PermissionRequestBloc>(),
|
||||||
|
),
|
||||||
|
BlocProvider<UpdateCubit>(
|
||||||
|
create: (_) => UpdateCubit(VersionCheckService()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<LocaleCubit, Locale>(
|
child: BlocBuilder<LocaleCubit, Locale>(
|
||||||
bloc: localeCubit,
|
bloc: localeCubit,
|
||||||
@@ -114,7 +118,8 @@ class _LifecycleListener extends StatefulWidget {
|
|||||||
State<_LifecycleListener> createState() => _LifecycleListenerState();
|
State<_LifecycleListener> createState() => _LifecycleListenerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LifecycleListenerState extends State<_LifecycleListener> with WidgetsBindingObserver {
|
class _LifecycleListenerState extends State<_LifecycleListener>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -172,7 +177,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is UpdateFailure) {
|
if (state is UpdateFailure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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: Center(
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 30),
|
margin: const EdgeInsets.symmetric(horizontal: 30),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('🎉 发现新版本',
|
const Text(
|
||||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
'🎉 发现新版本',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('版本号: ${state.versionInfo.version}',
|
Text(
|
||||||
style: const TextStyle(fontSize: 16)),
|
'版本号: ${state.versionInfo.version}',
|
||||||
Text('更新类型: ${state.versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}',
|
style: const TextStyle(fontSize: 16),
|
||||||
style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
),
|
||||||
|
Text(
|
||||||
|
'更新类型: ${state.versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (state.versionInfo.updateDesc.isNotEmpty) ...[
|
if (state.versionInfo.updateDesc.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
|
const Text(
|
||||||
|
'更新内容:',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
Text(state.versionInfo.updateDesc),
|
Text(state.versionInfo.updateDesc),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -215,20 +240,28 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.shade50,
|
color: Colors.blue.shade50,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: Colors.blue.shade200),
|
border: Border.all(
|
||||||
|
color: Colors.blue.shade200,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'💡 选择下载方式',
|
'💡 选择下载方式',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'• 自动安装:App 内下载并调起系统安装\n'
|
'• 自动安装: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: [
|
children: [
|
||||||
if (!state.versionInfo.forceUpdate)
|
if (!state.versionInfo.forceUpdate)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.read<UpdateCubit>().cancelUpdate(),
|
onPressed: () => context
|
||||||
|
.read<UpdateCubit>()
|
||||||
|
.cancelUpdate(),
|
||||||
child: const Text('稍后'),
|
child: const Text('稍后'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
@@ -266,7 +301,11 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
final info = state.versionInfo;
|
final info = state.versionInfo;
|
||||||
if (info.apkUrl != null) {
|
if (info.apkUrl != null) {
|
||||||
context.read<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
context
|
||||||
|
.read<UpdateCubit>()
|
||||||
|
.downloadAndInstallApk(
|
||||||
|
info.apkUrl!,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('手动下载'),
|
child: const Text('手动下载'),
|
||||||
@@ -276,7 +315,11 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
final info = state.versionInfo;
|
final info = state.versionInfo;
|
||||||
if (info.apkUrl != null) {
|
if (info.apkUrl != null) {
|
||||||
context.read<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
context
|
||||||
|
.read<UpdateCubit>()
|
||||||
|
.downloadAndInstallApk(
|
||||||
|
info.apkUrl!,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('自动安装'),
|
child: const Text('自动安装'),
|
||||||
@@ -307,7 +350,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'📱 整包更新步骤',
|
'📱 整包更新步骤',
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildStepItem(1, '在浏览器中下载 APK', true),
|
_buildStepItem(1, '在浏览器中下载 APK', true),
|
||||||
@@ -347,7 +393,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
state.isPatch ? '⬇️ 正在下载补丁...' : '⬇️ 正在下载 APK...',
|
state.isPatch ? '⬇️ 正在下载补丁...' : '⬇️ 正在下载 APK...',
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -362,7 +411,10 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${(state.progress * 100).toInt()}%',
|
'${(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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
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(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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 SizedBox(height: 16),
|
||||||
const Text('✅ 更新已完成',
|
const Text(
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
'✅ 更新已完成',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text(
|
||||||
'补丁已成功应用!\n\n请完全关闭 App 后重新打开,\n即可体验新版本功能。',
|
'补丁已成功应用!\n\n请完全关闭 App 后重新打开,\n即可体验新版本功能。',
|
||||||
@@ -435,8 +499,13 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text('状态: ${state.runtimeType.toString().replaceAll('Update', '')}',
|
Text(
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
'状态: ${state.runtimeType.toString().replaceAll('Update', '')}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
// 🔥 暂时关闭刷新按钮和点击阴影
|
// 🔥 暂时关闭刷新按钮和点击阴影
|
||||||
// IconButton(
|
// IconButton(
|
||||||
// icon: const Icon(Icons.refresh, color: Colors.white),
|
// icon: const Icon(Icons.refresh, color: Colors.white),
|
||||||
@@ -485,8 +554,7 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (completed)
|
if (completed) const Icon(Icons.check, color: Colors.green, size: 20),
|
||||||
const Icon(Icons.check, color: Colors.green, size: 20),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_svg: ^2.2.3
|
flutter_svg: ^2.2.3
|
||||||
|
vibration: ^3.1.8
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ dependencies:
|
|||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
video_player: ^2.8.2
|
video_player: ^2.8.2
|
||||||
|
|
||||||
|
# ===== 火山引擎 RTC 实时音视频 =====
|
||||||
|
#volc_engine_rtc: ^3.60.4
|
||||||
|
|
||||||
|
|
||||||
# ===== 屏幕适配,高刷等 =====
|
# ===== 屏幕适配,高刷等 =====
|
||||||
# flutter_displaymode: ^0.7.0
|
# flutter_displaymode: ^0.7.0
|
||||||
|
|||||||
Reference in New Issue
Block a user