机器人的列表接口

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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