告警中心的二级详情页面+信息中心的页面+对接机器人接口

This commit is contained in:
2026-06-02 08:40:50 +08:00
parent 9eab402551
commit d8879dad62
48 changed files with 4376 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import '../../domain/entities/drone_station_entity.dart';
abstract class DroneStationDataSource {
Future<List<DroneStationEntity>> getDroneStationList(int siteId);
Future<UAVDetailEntity> getUAVDetail(String gatewaySn, String deviceSn);
Future<VideoStreamEntity> getVideoStream({
required String sn,
required String cameraIndex,
required String cameraPosition,
String qualityType = 'adaptive',
int videoExpire = 7200,
});
}

View File

@@ -0,0 +1,86 @@
import 'package:dio/dio.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import '../datasources/drone_station_datasource.dart';
import '../../domain/entities/drone_station_entity.dart';
class DroneStationDataSourceImpl implements DroneStationDataSource {
final Dio dio;
DroneStationDataSourceImpl(this.dio);
@override
Future<List<DroneStationEntity>> getDroneStationList(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) => DroneStationEntity.fromJson(item)).toList();
}
@override
Future<UAVDetailEntity> getUAVDetail(
String gatewaySn,
String deviceSn,
) async {
final response = await dio.get(
HttpApiConsts.getUAVState,
queryParameters: {'sn': gatewaySn, 'droneSn': deviceSn},
);
if (response.statusCode != 200) {
throw Exception('网络请求失败: ${response.statusCode}');
}
final responseData = response.data;
if (responseData['code'] != 200) {
throw Exception(responseData['msg'] ?? '业务异常');
}
return UAVDetailEntity.fromJson(responseData['data']);
}
@override
Future<VideoStreamEntity> getVideoStream({
required String sn,
required String cameraIndex,
required String cameraPosition,
String qualityType = 'adaptive',
int videoExpire = 7200,
}) async {
final response = await dio.post(
HttpApiConsts.changeCamera,
data: {
'sn': sn,
'cameraIndex': cameraIndex,
'cameraPosition': cameraPosition,
'qualityType': qualityType,
'videoExpire': videoExpire,
},
);
if (response.statusCode != 200) {
throw Exception('网络请求失败: ${response.statusCode}');
}
final responseData = response.data;
if (responseData['code'] != 200) {
throw Exception(responseData['msg'] ?? '业务异常');
}
return VideoStreamEntity.fromJson(responseData['data']);
}
}

View File

@@ -0,0 +1,57 @@
/// 机器人数据模型
class RobotDataModel {
final String name;
final String id;
final String type;
final String status;
final double battery;
final String task;
const RobotDataModel({
required this.name,
required this.id,
required this.type,
required this.status,
required this.battery,
required this.task,
});
/// 从 JSON 创建数据模型
factory RobotDataModel.fromJson(Map<String, dynamic> json) {
return RobotDataModel(
name: json['deviceName'] ?? json['name'] ?? '',
id: json['deviceId']?.toString() ?? json['id']?.toString() ?? '',
type: json['deviceTypeName'] ?? json['type'] ?? '未知类型',
status: _getStatusText(json['status'] ?? 0),
battery: (json['battery'] as num?)?.toDouble() ??
(json['capacity_percent'] as num?)?.toDouble() ?? 0.0,
task: json['task'] ?? json['currentTask'] ?? '待机中',
);
}
/// 转换为 JSON
Map<String, dynamic> toJson() {
return {
'name': name,
'id': id,
'type': type,
'status': status,
'battery': battery,
'task': task,
};
}
/// 状态码转换
static String _getStatusText(int status) {
switch (status) {
case 1:
return '在线';
case 2:
return '离线';
case 3:
return '异常';
default:
return '未知';
}
}
}

View File

@@ -0,0 +1,54 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../datasources/drone_station_datasource.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../../domain/repositories/drone_station_repository.dart';
class DroneStationRepositoryImpl implements DroneStationRepository {
final DroneStationDataSource dataSource;
DroneStationRepositoryImpl(this.dataSource);
@override
Future<Either<Failure, List<DroneStationEntity>>> getDroneStationList(
int siteId,
) async {
try {
final stations = await dataSource.getDroneStationList(siteId);
return Right(stations);
} catch (e) {
return Left(Failure(e.toString()));
}
}
@override
Future<Either<Failure, UAVDetailEntity>> getUAVDetail(
String gatewaySn,
String deviceSn,
) async {
try {
final detail = await dataSource.getUAVDetail(gatewaySn, deviceSn);
return Right(detail);
} catch (e) {
return Left(Failure(e.toString()));
}
}
@override
Future<Either<Failure, VideoStreamEntity>> getVideoStream({
required String sn,
required String cameraIndex,
required String cameraPosition,
}) async {
try {
final videoStream = await dataSource.getVideoStream(
sn: sn,
cameraIndex: cameraIndex,
cameraPosition: cameraPosition,
);
return Right(videoStream);
} catch (e) {
return Left(Failure(e.toString()));
}
}
}

View File

@@ -0,0 +1,420 @@
import 'package:equatable/equatable.dart';
/// 摄像头信息
class CameraInfo extends Equatable {
final String cameraIndex;
final List<String>? availableCameraPositions;
final String cameraPosition;
const CameraInfo({
required this.cameraIndex,
this.availableCameraPositions,
required this.cameraPosition,
});
factory CameraInfo.fromJson(Map<String, dynamic> json) {
return CameraInfo(
cameraIndex: json['camera_index'] ?? '',
availableCameraPositions: json['available_camera_positions'] != null
? List<String>.from(json['available_camera_positions'])
: null,
cameraPosition: json['camera_position'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'camera_index': cameraIndex,
'available_camera_positions': availableCameraPositions,
'camera_position': cameraPosition,
};
}
@override
List<Object?> get props => [
cameraIndex,
availableCameraPositions,
cameraPosition,
];
}
/// 位置状态
class PositionState extends Equatable {
final int gpsNumber;
final String isFixed;
final String quality;
final int rtkNumber;
const PositionState({
required this.gpsNumber,
required this.isFixed,
required this.quality,
required this.rtkNumber,
});
factory PositionState.fromJson(Map<String, dynamic> json) {
return PositionState(
gpsNumber: json['gps_number'] ?? 0,
isFixed: json['is_fixed'] ?? '',
quality: json['quality'] ?? '',
rtkNumber: json['rtk_number'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'gps_number': gpsNumber,
'is_fixed': isFixed,
'quality': quality,
'rtk_number': rtkNumber,
};
}
@override
List<Object?> get props => [gpsNumber, isFixed, quality, rtkNumber];
}
/// UAV详情实体(用于详情页面API返回的数据)
class UAVDetailEntity extends Equatable {
final String deviceSn;
final String gatewaySn;
final String callsign;
final String droneCallsign;
final int onlineStatus;
final int droneOnlineStatus;
final double? latitude;
final double? longitude;
final double? capacityPercent;
final double? windSpeed;
final double? height;
final double? environmentTemperature;
final dynamic networkState;
final PositionState? positionState;
final double? homeDistance;
final double? liveCapacity;
final String? rainfall;
final List<CameraInfo>? gatewayCameraList;
final List<dynamic>? droneCameraList;
final int? orgId;
final int? siteId;
final int? userId;
const UAVDetailEntity({
required this.deviceSn,
required this.gatewaySn,
required this.callsign,
required this.droneCallsign,
required this.onlineStatus,
required this.droneOnlineStatus,
this.latitude,
this.longitude,
this.capacityPercent,
this.windSpeed,
this.height,
this.environmentTemperature,
this.networkState,
this.positionState,
this.homeDistance,
this.liveCapacity,
this.rainfall,
this.gatewayCameraList,
this.droneCameraList,
this.orgId,
this.siteId,
this.userId,
});
factory UAVDetailEntity.fromJson(Map<String, dynamic> json) {
return UAVDetailEntity(
deviceSn: json['device_sn'] ?? '',
gatewaySn: json['gateway_sn'] ?? '',
callsign: json['callsign'] ?? '',
droneCallsign: json['drone_callsign'] ?? '',
onlineStatus: json['onlineStatus'] ?? 0,
droneOnlineStatus: json['drone_onlineStatus'] ?? 0,
latitude: json['latitude'] != null
? (json['latitude'] as num).toDouble()
: null,
longitude: json['longitude'] != null
? (json['longitude'] as num).toDouble()
: null,
capacityPercent: json['capacity_percent'] != null
? (json['capacity_percent'] as num).toDouble()
: null,
windSpeed: json['wind_speed'] != null
? (json['wind_speed'] as num).toDouble()
: null,
height: json['height'] != null
? (json['height'] as num).toDouble()
: null,
environmentTemperature: json['environment_temperature'] != null
? (json['environment_temperature'] as num).toDouble()
: null,
networkState: json['network_state'],
positionState: json['position_state'] != null
? PositionState.fromJson(json['position_state'])
: null,
homeDistance: json['home_distance'] != null
? (json['home_distance'] as num).toDouble()
: null,
liveCapacity: json['live_capacity'] != null
? (json['live_capacity'] as num).toDouble()
: null,
rainfall: json['rainfall']?.toString(),
gatewayCameraList: json['gateway_camera_list'] != null
? (json['gateway_camera_list'] as List)
.map((item) => CameraInfo.fromJson(item))
.toList()
: null,
droneCameraList: json['drone_camera_list'],
orgId: json['orgId'],
siteId: json['siteId'],
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'device_sn': deviceSn,
'gateway_sn': gatewaySn,
'callsign': callsign,
'drone_callsign': droneCallsign,
'onlineStatus': onlineStatus,
'drone_onlineStatus': droneOnlineStatus,
'latitude': latitude,
'longitude': longitude,
'capacity_percent': capacityPercent,
'wind_speed': windSpeed,
'height': height,
'environment_temperature': environmentTemperature,
'network_state': networkState,
'position_state': positionState?.toJson(),
'home_distance': homeDistance,
'live_capacity': liveCapacity,
'rainfall': rainfall,
'gateway_camera_list': gatewayCameraList?.map((c) => c.toJson()).toList(),
'drone_camera_list': droneCameraList,
'orgId': orgId,
'siteId': siteId,
'userId': userId,
};
}
bool get isOnline => onlineStatus == 1;
bool get isDroneOnline => droneOnlineStatus == 1;
@override
List<Object?> get props => [
deviceSn,
gatewaySn,
callsign,
droneCallsign,
onlineStatus,
droneOnlineStatus,
latitude,
longitude,
capacityPercent,
windSpeed,
height,
environmentTemperature,
networkState,
positionState,
homeDistance,
liveCapacity,
rainfall,
gatewayCameraList,
droneCameraList,
orgId,
siteId,
userId,
];
}
/// 无人机机场实体
class DroneStationEntity extends Equatable {
final String deviceSn; // 机场设备序列号
final String gatewaySn; // 网关序列号
final String callsign; // 机场呼号/名称
final String droneCallsign; // 无人机呼号
final int onlineStatus; // 机场在线状态 (1:在线, 0:离线)
final int droneOnlineStatus; // 无人机在线状态
final double? latitude; // 纬度
final double? longitude; // 经度
final double? capacityPercent; // 电量百分比
final double? windSpeed; // 风速
final double? height; // 高度
final double? environmentTemperature; // 环境温度
final int? networkState; // 网络状态
final int? positionState; // 位置状态
final double? homeDistance; // 距离home点距离
final double? liveCapacity; // 实时容量
final double? rainfall; // 降雨量
final List<CameraInfo>? gatewayCameraList; // 网关摄像头列表
final List<dynamic>? droneCameraList; // 无人机摄像头列表
final int orgId; // 组织ID
final int siteId; // 场站ID
final int? userId; // 用户ID
const DroneStationEntity({
required this.deviceSn,
required this.gatewaySn,
required this.callsign,
required this.droneCallsign,
required this.onlineStatus,
required this.droneOnlineStatus,
this.latitude,
this.longitude,
this.capacityPercent,
this.windSpeed,
this.height,
this.environmentTemperature,
this.networkState,
this.positionState,
this.homeDistance,
this.liveCapacity,
this.rainfall,
this.gatewayCameraList,
this.droneCameraList,
required this.orgId,
required this.siteId,
this.userId,
});
factory DroneStationEntity.fromJson(Map<String, dynamic> json) {
return DroneStationEntity(
deviceSn: json['device_sn'] ?? '',
gatewaySn: json['gateway_sn'] ?? '',
callsign: json['callsign'] ?? '',
droneCallsign: json['drone_callsign'] ?? '',
onlineStatus: json['onlineStatus'] ?? 0,
droneOnlineStatus: json['drone_onlineStatus'] ?? 0,
latitude: json['latitude'] != null
? (json['latitude'] as num).toDouble()
: null,
longitude: json['longitude'] != null
? (json['longitude'] as num).toDouble()
: null,
capacityPercent: json['capacity_percent'] != null
? (json['capacity_percent'] as num).toDouble()
: null,
windSpeed: json['wind_speed'] != null
? (json['wind_speed'] as num).toDouble()
: null,
height: json['height'] != null
? (json['height'] as num).toDouble()
: null,
environmentTemperature: json['environment_temperature'] != null
? (json['environment_temperature'] as num).toDouble()
: null,
networkState: json['network_state'],
positionState: json['position_state'],
homeDistance: json['home_distance'] != null
? (json['home_distance'] as num).toDouble()
: null,
liveCapacity: json['live_capacity'] != null
? (json['live_capacity'] as num).toDouble()
: null,
rainfall: json['rainfall'] != null
? (json['rainfall'] as num).toDouble()
: null,
gatewayCameraList: json['gateway_camera_list'] != null
? (json['gateway_camera_list'] as List)
.map((item) => CameraInfo.fromJson(item))
.toList()
: null,
droneCameraList: json['drone_camera_list'],
orgId: json['orgId'] ?? 0,
siteId: json['siteId'] ?? 0,
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'device_sn': deviceSn,
'gateway_sn': gatewaySn,
'callsign': callsign,
'drone_callsign': droneCallsign,
'onlineStatus': onlineStatus,
'drone_onlineStatus': droneOnlineStatus,
'latitude': latitude,
'longitude': longitude,
'capacity_percent': capacityPercent,
'wind_speed': windSpeed,
'height': height,
'environment_temperature': environmentTemperature,
'network_state': networkState,
'position_state': positionState,
'home_distance': homeDistance,
'live_capacity': liveCapacity,
'rainfall': rainfall,
'gateway_camera_list': gatewayCameraList?.map((c) => c.toJson()).toList(),
'drone_camera_list': droneCameraList,
'orgId': orgId,
'siteId': siteId,
'userId': userId,
};
}
/// 是否在线
bool get isOnline => onlineStatus == 1;
/// 无人机是否在线
bool get isDroneOnline => droneOnlineStatus == 1;
@override
List<Object?> get props => [
deviceSn,
gatewaySn,
callsign,
droneCallsign,
onlineStatus,
droneOnlineStatus,
latitude,
longitude,
capacityPercent,
windSpeed,
height,
environmentTemperature,
networkState,
positionState,
homeDistance,
liveCapacity,
rainfall,
gatewayCameraList,
droneCameraList,
orgId,
siteId,
userId,
];
}
/// 视频流实体
class VideoStreamEntity extends Equatable {
final String sn;
final String cameraIndex;
final String url;
final int expireTs;
final String urlType;
const VideoStreamEntity({
required this.sn,
required this.cameraIndex,
required this.url,
required this.expireTs,
required this.urlType,
});
factory VideoStreamEntity.fromJson(Map<String, dynamic> json) {
return VideoStreamEntity(
sn: json['sn'] ?? '',
cameraIndex: json['camera_index'] ?? '',
url: json['url'] ?? '',
expireTs: json['expire_ts'] ?? 0,
urlType: json['url_type'] ?? '',
);
}
@override
List<Object?> get props => [sn, cameraIndex, url, expireTs, urlType];
}

View File

@@ -0,0 +1,59 @@
import 'package:equatable/equatable.dart';
/// 视频流实体
class VideoStreamEntity extends Equatable {
final String sn;
final String cameraIndex;
final String
url; // 火山引擎 RTC 鉴权参数(包含 app_id、room_id、token、user_id、expire_time)
final int expireTs;
final String urlType; // 标识是 "rtc" 还是其他类型
const VideoStreamEntity({
required this.sn,
required this.cameraIndex,
required this.url,
required this.expireTs,
required this.urlType,
});
factory VideoStreamEntity.fromJson(Map<String, dynamic> json) {
return VideoStreamEntity(
sn: json['sn'] ?? '',
cameraIndex: json['camera_index'] ?? '',
url: json['url'] ?? '', // 火山引擎 RTC 鉴权参数
expireTs: json['expire_ts'] ?? 0,
urlType: json['url_type'] ?? 'rtc', // 默认为 rtc 类型
);
}
/// 解析火山引擎 RTC 参数
/// url 格式示例: "app_id=xxx&room_id=xxx&token=xxx&user_id=xxx&expire_time=xxx"
Map<String, String> parseRtcParams() {
final params = <String, String>{};
final pairs = url.split('&');
for (final pair in pairs) {
final kv = pair.split('=');
if (kv.length == 2) {
// 对 URL 编码的字符进行解码(如 %2F -> /, %2B -> +)
params[kv[0]] = Uri.decodeComponent(kv[1]);
}
}
return params;
}
/// 获取火山引擎 App ID(从 URL 中解析)
String get appId => parseRtcParams()['app_id'] ?? '';
/// 获取房间 ID(从 URL 中解析)
String get roomId => parseRtcParams()['room_id'] ?? '';
/// 获取 Token(从 URL 中解析)
String get token => parseRtcParams()['token'] ?? '';
/// 获取用户 ID(从 URL 中解析)
String get userId => parseRtcParams()['user_id'] ?? '';
@override
List<Object?> get props => [sn, cameraIndex, url, expireTs, urlType];
}

View File

@@ -0,0 +1,18 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../entities/drone_station_entity.dart';
abstract class DroneStationRepository {
Future<Either<Failure, List<DroneStationEntity>>> getDroneStationList(
int siteId,
);
Future<Either<Failure, UAVDetailEntity>> getUAVDetail(
String gatewaySn,
String deviceSn,
);
Future<Either<Failure, VideoStreamEntity>> getVideoStream({
required String sn,
required String cameraIndex,
required String cameraPosition,
});
}

View File

@@ -0,0 +1,24 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../entities/drone_station_entity.dart';
import '../repositories/drone_station_repository.dart';
class GetDroneStationListUseCase {
final DroneStationRepository repository;
GetDroneStationListUseCase(this.repository);
Future<Either<Failure, List<DroneStationEntity>>> call(int siteId) async {
return await repository.getDroneStationList(siteId);
}
}
class GetUAVDetailUseCase {
final DroneStationRepository repository;
GetUAVDetailUseCase(this.repository);
Future<Either<Failure, UAVDetailEntity>> call(String gatewaySn, String deviceSn) async {
return await repository.getUAVDetail(gatewaySn, deviceSn);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../entities/drone_station_entity.dart';
import '../repositories/drone_station_repository.dart';
class GetVideoStreamUseCase {
final DroneStationRepository repository;
GetVideoStreamUseCase(this.repository);
Future<Either<Failure, VideoStreamEntity>> call({
required String sn,
required String cameraIndex,
required String cameraPosition,
}) async {
return await repository.getVideoStream(
sn: sn,
cameraIndex: cameraIndex,
cameraPosition: cameraPosition,
);
}
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/get_drone_station_list_usecase.dart';
import '../../domain/usecases/get_video_stream_usecase.dart';
import 'drone_station_event.dart';
import 'drone_station_state.dart';
class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
final GetDroneStationListUseCase getDroneStationListUseCase;
final GetUAVDetailUseCase getUAVDetailUseCase;
final GetVideoStreamUseCase getVideoStreamUseCase;
DroneStationBloc(
this.getDroneStationListUseCase,
this.getUAVDetailUseCase,
this.getVideoStreamUseCase,
) : super(const DroneStationInitial()) {
on<DroneStationLoadData>(_onLoadData);
on<DroneStationRefresh>(_onRefresh);
on<UAVDetailLoad>(_onUAVDetailLoad);
on<VideoStreamLoad>(_onVideoStreamLoad);
}
Future<void> _onLoadData(
DroneStationLoadData event,
Emitter<DroneStationState> emit,
) async {
emit(const DroneStationLoading());
final result = await getDroneStationListUseCase(event.siteId);
result.fold(
(failure) => emit(DroneStationError(failure.message)),
(stations) => emit(DroneStationLoaded(stations)),
);
}
Future<void> _onRefresh(
DroneStationRefresh event,
Emitter<DroneStationState> emit,
) async {
if (state is DroneStationLoaded) {
final result = await getDroneStationListUseCase(event.siteId);
result.fold(
(failure) => emit(DroneStationError(failure.message)),
(stations) => emit(DroneStationLoaded(stations)),
);
}
}
Future<void> _onUAVDetailLoad(
UAVDetailLoad event,
Emitter<DroneStationState> emit,
) async {
emit(const UAVDetailLoading());
final result = await getUAVDetailUseCase(event.gatewaySn, event.deviceSn);
result.fold(
(failure) => emit(UAVDetailError(failure.message)),
(detail) => emit(UAVDetailLoaded(detail)),
);
}
Future<void> _onVideoStreamLoad(
VideoStreamLoad event,
Emitter<DroneStationState> emit,
) async {
emit(const VideoStreamLoading());
final result = await getVideoStreamUseCase(
sn: event.sn,
cameraIndex: event.cameraIndex,
cameraPosition: event.cameraPosition,
);
result.fold(
(failure) => emit(VideoStreamError(failure.message)),
(videoStream) => emit(VideoStreamLoaded(videoStream)),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:equatable/equatable.dart';
abstract class DroneStationEvent extends Equatable {
const DroneStationEvent();
@override
List<Object?> get props => [];
}
class DroneStationLoadData extends DroneStationEvent {
final int siteId;
const DroneStationLoadData(this.siteId);
@override
List<Object?> get props => [siteId];
}
class DroneStationRefresh extends DroneStationEvent {
final int siteId;
const DroneStationRefresh(this.siteId);
@override
List<Object?> get props => [siteId];
}
class UAVDetailLoad extends DroneStationEvent {
final String gatewaySn;
final String deviceSn;
const UAVDetailLoad({required this.gatewaySn, required this.deviceSn});
@override
List<Object?> get props => [gatewaySn, deviceSn];
}
class VideoStreamLoad extends DroneStationEvent {
final String sn;
final String cameraIndex;
final String cameraPosition;
const VideoStreamLoad({
required this.sn,
required this.cameraIndex,
required this.cameraPosition,
});
@override
List<Object?> get props => [sn, cameraIndex, cameraPosition];
}

View File

@@ -0,0 +1,79 @@
import 'package:equatable/equatable.dart';
import '../../domain/entities/drone_station_entity.dart';
abstract class DroneStationState extends Equatable {
const DroneStationState();
@override
List<Object?> get props => [];
}
class DroneStationInitial extends DroneStationState {
const DroneStationInitial();
}
class DroneStationLoading extends DroneStationState {
const DroneStationLoading();
}
class DroneStationLoaded extends DroneStationState {
final List<DroneStationEntity> stations;
const DroneStationLoaded(this.stations);
@override
List<Object?> get props => [stations];
}
class DroneStationError extends DroneStationState {
final String message;
const DroneStationError(this.message);
@override
List<Object?> get props => [message];
}
class UAVDetailLoading extends DroneStationState {
const UAVDetailLoading();
}
class UAVDetailLoaded extends DroneStationState {
final UAVDetailEntity detail;
const UAVDetailLoaded(this.detail);
@override
List<Object?> get props => [detail];
}
class UAVDetailError extends DroneStationState {
final String message;
const UAVDetailError(this.message);
@override
List<Object?> get props => [message];
}
class VideoStreamLoading extends DroneStationState {
const VideoStreamLoading();
}
class VideoStreamLoaded extends DroneStationState {
final VideoStreamEntity videoStream;
const VideoStreamLoaded(this.videoStream);
@override
List<Object?> get props => [videoStream];
}
class VideoStreamError extends DroneStationState {
final String message;
const VideoStreamError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,111 @@
import 'package:dio/dio.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/consts/http_api_consts.dart';
import '../../data/models/robot_data_model.dart';
import 'robot_list_event.dart';
import 'robot_list_state.dart';
class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
final Dio dio;
RobotListBloc(this.dio) : super(const RobotListInitial()) {
on<RobotListLoadData>(_onLoadData);
on<RobotListRefresh>(_onRefresh);
on<RobotListChangeType>(_onChangeType);
}
Future<void> _onLoadData(
RobotListLoadData event,
Emitter<RobotListState> emit,
) async {
if (event.siteId == null) {
emit(const RobotListError('请先选择场站'));
return;
}
emit(const RobotListLoading());
try {
final response = await dio.get(
HttpApiConsts.getRobotList,
queryParameters: {
'siteId': event.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'] ?? [];
final robots = rows.map((item) => RobotDataModel.fromJson(item)).toList();
emit(RobotListLoaded(
robots: robots,
siteId: event.siteId,
));
} catch (e) {
emit(RobotListError(e.toString()));
}
}
Future<void> _onRefresh(
RobotListRefresh event,
Emitter<RobotListState> emit,
) async {
if (state is RobotListLoaded) {
final currentState = state as RobotListLoaded;
if (currentState.siteId == null) {
emit(const RobotListError('请先选择场站'));
return;
}
try {
final response = await dio.get(
HttpApiConsts.getRobotList,
queryParameters: {
'siteId': currentState.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'] ?? [];
final robots = rows.map((item) => RobotDataModel.fromJson(item)).toList();
emit(currentState.copyWith(robots: robots));
} catch (e) {
emit(RobotListError(e.toString()));
}
}
}
void _onChangeType(
RobotListChangeType event,
Emitter<RobotListState> emit,
) {
if (state is RobotListLoaded) {
final currentState = state as RobotListLoaded;
emit(currentState.copyWith(selectedType: event.type));
}
}
}

View File

@@ -0,0 +1,30 @@
import 'package:equatable/equatable.dart';
abstract class RobotListEvent extends Equatable {
const RobotListEvent();
@override
List<Object?> get props => [];
}
class RobotListLoadData extends RobotListEvent {
final int? siteId;
const RobotListLoadData({this.siteId});
@override
List<Object?> get props => [siteId];
}
class RobotListRefresh extends RobotListEvent {
const RobotListRefresh();
}
class RobotListChangeType extends RobotListEvent {
final String? type;
const RobotListChangeType(this.type);
@override
List<Object?> get props => [type];
}

View File

@@ -0,0 +1,53 @@
import 'package:equatable/equatable.dart';
import '../../data/models/robot_data_model.dart';
abstract class RobotListState extends Equatable {
const RobotListState();
@override
List<Object?> get props => [];
}
class RobotListInitial extends RobotListState {
const RobotListInitial();
}
class RobotListLoading extends RobotListState {
const RobotListLoading();
}
class RobotListLoaded extends RobotListState {
final List<RobotDataModel> robots;
final String? selectedType;
final int? siteId;
const RobotListLoaded({
required this.robots,
this.selectedType,
this.siteId,
});
RobotListLoaded copyWith({
List<RobotDataModel>? robots,
String? selectedType,
int? siteId,
}) {
return RobotListLoaded(
robots: robots ?? this.robots,
selectedType: selectedType ?? this.selectedType,
siteId: siteId ?? this.siteId,
);
}
@override
List<Object?> get props => [robots, selectedType, siteId];
}
class RobotListError extends RobotListState {
final String message;
const RobotListError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,377 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// import 'package:volc_engine_rtc/volc_engine_rtc.dart';
import '../../../../../core/di/injection.dart';
import '../bloc/drone_station_bloc.dart';
import '../bloc/drone_station_event.dart';
import '../bloc/drone_station_state.dart';
class DroneMonitorPage extends StatefulWidget {
final String gatewaySn;
final String cameraIndex;
const DroneMonitorPage({
super.key,
required this.gatewaySn,
required this.cameraIndex,
});
@override
_DroneMonitorPageState createState() => _DroneMonitorPageState();
}
class _DroneMonitorPageState extends State<DroneMonitorPage> {
bool isIndoor = false;
late DroneStationBloc _bloc;
String? videoUrl;
bool isLoading = true;
String? errorMessage;
// RTCEngine? _rtcEngine;
// RTCRoom? _rtcRoom;
// RTCViewContext? _remoteRenderContext;
// final IRTCEngineEventHandler _videoHandler = IRTCEngineEventHandler();
// final IRTCRoomEventHandler _roomHandler = IRTCRoomEventHandler();
String? _appId;
String? _roomId;
String? _token;
String? _userId;
bool _isJoined = false;
@override
void initState() {
super.initState();
_bloc = sl<DroneStationBloc>();
// _initVideoEventHandler();
// _initRoomEventHandler();
_loadVideoStream();
}
// void _initVideoEventHandler() {
// _videoHandler.onFirstRemoteVideoFrameDecoded =
// (String streamId, StreamInfo streamInfo, VideoFrameInfo frameInfo) {
// debugPrint('onFirstRemoteVideoFrameDecoded: ${streamInfo.userId}');
// if (streamInfo.userId.isNotEmpty && mounted && _roomId != null) {
// setState(() {
// _remoteRenderContext = RTCViewContext.remoteContext(
// roomId: _roomId!,
// userId: streamInfo.userId,
// streamId: streamId,
// );
// });
// }
// };
// _videoHandler.onWarning = (WarningCode code) {
// debugPrint('warningCode: $code');
// };
// _videoHandler.onError = (ErrorCode code) {
// debugPrint('errorCode: $code');
// if (mounted) {
// setState(() {
// errorMessage = '视频错误: $code';
// isLoading = false;
// });
// }
// };
// }
// void _initRoomEventHandler() {
// _roomHandler.onUserJoined = (UserInfo userInfo) {
// debugPrint('onUserJoined: ${userInfo.userId}');
// };
// _roomHandler.onUserLeave = (String uid, int reason) {
// debugPrint('onUserLeave: $uid reason: $reason');
// };
// }
@override
void dispose() {
_bloc.close();
_leaveRoom();
super.dispose();
}
void _loadVideoStream() {
setState(() {
isLoading = true;
errorMessage = null;
});
_leaveRoom();
_bloc.add(
VideoStreamLoad(
sn: widget.gatewaySn,
cameraIndex: widget.cameraIndex,
cameraPosition: isIndoor ? 'indoor' : 'outdoor',
),
);
}
void _onCameraPositionChanged(bool indoor) {
setState(() {
isIndoor = indoor;
});
_loadVideoStream();
}
void _parseUrlParams(String url) {
try {
final uri = Uri.parse(url.contains('?') ? 'http://host?$url' : url);
final params = uri.queryParameters;
_appId = params['app_id'];
_roomId = params['room_id'];
_token = params['token'];
_userId =
params['user_id'] ?? 'user_${DateTime.now().millisecondsSinceEpoch}';
print(
'Parsed RTC params: appId=$_appId, roomId=$_roomId, userId=$_userId',
);
} catch (e) {
print('Failed to parse URL: $e');
errorMessage = '解析视频参数失败';
isLoading = false;
}
}
// Future<void> _joinRoom() async {
// if (_appId == null || _roomId == null || _token == null) {
// return;
// }
// try {
// _rtcEngine = await RTCEngine.createRTCEngine(
// RTCVideoContext(appId: _appId!, eventHandler: _videoHandler),
// );
// if (_rtcEngine == null) {
// setState(() {
// errorMessage = '创建引擎失败';
// isLoading = false;
// });
// return;
// }
// _rtcRoom = await _rtcEngine?.createRTCRoom(_roomId!);
// _rtcRoom?.setRTCRoomEventHandler(_roomHandler);
// final userInfo = UserInfo(userId: _userId!, extraInfo: "");
// final roomConfig = RoomConfig(
// isPublishAudio: false,
// isPublishVideo: false,
// isAutoSubscribeAudio: true,
// isAutoSubscribeVideo: true,
// );
// await _rtcRoom?.joinRoom(
// token: _token!,
// userInfo: userInfo,
// roomConfig: roomConfig,
// userVisibility: true,
// );
// setState(() {
// _isJoined = true;
// });
// print('Successfully joined room: $_roomId');
// } catch (e) {
// print('Failed to join room: $e');
// setState(() {
// errorMessage = '加入房间失败: $e';
// isLoading = false;
// });
// }
// }
Future<void> _leaveRoom() async {
try {
// if (_rtcRoom != null) {
// await _rtcRoom!.leaveRoom();
// _rtcRoom = null;
// }
// if (_rtcEngine != null) {
// _rtcEngine!.destroy();
// _rtcEngine = null;
// }
setState(() {
_isJoined = false;
});
} catch (e) {
print('Failed to leave room: $e');
}
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
child: Scaffold(
backgroundColor: const Color(0xFF1D2129),
appBar: AppBar(
backgroundColor: const Color(0xFF2A2E34),
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text('实时监控', style: TextStyle(color: Colors.white)),
),
body: Column(
children: [
Expanded(
child: BlocConsumer<DroneStationBloc, DroneStationState>(
listener: (context, state) async {
if (state is VideoStreamLoaded) {
videoUrl = state.videoStream.url;
_parseUrlParams(videoUrl!);
// await _joinRoom();
if (mounted) {
setState(() {
isLoading = false;
});
}
} else if (state is VideoStreamError) {
if (mounted) {
setState(() {
isLoading = false;
errorMessage = state.message;
});
}
}
},
builder: (context, state) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
errorMessage!,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadVideoStream,
child: const Text('重试'),
),
],
),
);
}
return _buildVideoView();
},
),
),
Container(
padding: const EdgeInsets.all(16),
color: const Color(0xFF2A2E34),
child: Row(
children: [
Expanded(
child: _buildBtn(
Icons.home,
'室内',
isIndoor,
() => _onCameraPositionChanged(true),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildBtn(
Icons.sunny,
'室外',
!isIndoor,
() => _onCameraPositionChanged(false),
),
),
],
),
),
],
),
),
);
}
Widget _buildVideoView() {
// if (!_isJoined || _rtcEngine == null || _userId == null) {
// return Center(
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// const Icon(Icons.video_library, size: 80, color: Colors.grey),
// const SizedBox(height: 16),
// const Text(
// '正在连接...',
// style: TextStyle(color: Colors.white, fontSize: 16),
// ),
// const SizedBox(height: 8),
// Text(
// '房间: $_roomId',
// style: const TextStyle(color: Colors.grey, fontSize: 12),
// ),
// ],
// ),
// );
// }
return Container(
color: Colors.black,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.videocam_off, size: 80, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'RTC 功能已禁用',
style: TextStyle(color: Colors.white, fontSize: 16),
),
const SizedBox(height: 8),
Text(
'火山引擎 RTC 依赖已注释',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
);
}
Widget _buildBtn(
IconData icon,
String label,
bool active,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: active ? const Color(0xFF165DFF) : const Color(0xFF3A3E44),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(icon, color: Colors.white),
const SizedBox(height: 8),
Text(label, style: const TextStyle(color: Colors.white)),
],
),
),
);
}
}

View File

@@ -0,0 +1,5 @@
import '../../domain/entities/site_entity.dart';
abstract class SiteDataSource {
Future<List<SiteEntity>> getSiteList(int orgId);
}

View File

@@ -0,0 +1,41 @@
import 'package:dio/dio.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import 'package:maibu_satabot_v2/features/v2/home/data/datasources/site_datasource.dart';
import 'package:maibu_satabot_v2/features/v2/home/domain/entities/site_entity.dart';
class SiteDataSourceImpl implements SiteDataSource {
final Dio dio;
SiteDataSourceImpl(this.dio);
@override
Future<List<SiteEntity>> getSiteList(int orgId) async {
// 构建查询参数:orgId 为 0 时不传递
final queryParams = <String, dynamic>{
'pageNum': 1,
'pageSize': 9999,
};
if (orgId != 0) {
queryParams['orgId'] = orgId;
}
final response = await dio.get(
HttpApiConsts.getSiteList,
queryParameters: queryParams,
);
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) => SiteEntity.fromJson(item)).toList();
}
}

View File

@@ -0,0 +1,22 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../../data/datasources/site_datasource.dart';
import '../../domain/entities/site_entity.dart';
import '../../domain/repositories/site_repository.dart';
class SiteRepositoryImpl implements SiteRepository {
final SiteDataSource dataSource;
SiteRepositoryImpl(this.dataSource);
@override
Future<Either<Failure, List<SiteEntity>>> getSiteList(int orgId) async {
try {
final sites = await dataSource.getSiteList(orgId);
return Right(sites);
} catch (e) {
return Left(Failure(e.toString()));
}
}
}

View File

@@ -0,0 +1,52 @@
import 'package:equatable/equatable.dart';
class SiteEntity extends Equatable {
final int id;
final String siteName;
final String? siteCode;
final int orgId;
final double? longitude;
final double? latitude;
final String? address;
final int status;
const SiteEntity({
required this.id,
required this.siteName,
this.siteCode,
required this.orgId,
this.longitude,
this.latitude,
this.address,
required this.status,
});
factory SiteEntity.fromJson(Map<String, dynamic> json) {
return SiteEntity(
id: json['id'] as int,
siteName: json['siteName'] as String? ?? '',
siteCode: json['siteCode'] as String?,
orgId: json['orgId'] as int? ?? 0,
longitude: (json['longitude'] as num?)?.toDouble(),
latitude: (json['latitude'] as num?)?.toDouble(),
address: json['address'] as String?,
status: json['status'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'siteName': siteName,
'siteCode': siteCode,
'orgId': orgId,
'longitude': longitude,
'latitude': latitude,
'address': address,
'status': status,
};
}
@override
List<Object?> get props => [id, siteName, siteCode, orgId, longitude, latitude, address, status];
}

View File

@@ -0,0 +1,9 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../entities/site_entity.dart';
abstract class SiteRepository {
Future<Either<Failure, List<SiteEntity>>> getSiteList(int orgId);
}

View File

@@ -0,0 +1,17 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../entities/site_entity.dart';
import '../repositories/site_repository.dart';
class GetSiteListUseCase {
final SiteRepository repository;
GetSiteListUseCase(this.repository);
// pageNum 和 pageSize 固定,orgId 从登录用户信息中获取
Future<Either<Failure, List<SiteEntity>>> call(int orgId) async {
return await repository.getSiteList(orgId);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:maibu_satabot_v2/features/v2/message_center/data/datasources/message_center_remote_datasource.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/data/models/message_category_model.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
/// 消息中心远程数据源实现(模拟数据)
class MessageCenterRemoteDataSourceImpl implements MessageCenterRemoteDataSource {
@override
Future<List<MessageCategoryModel>> getMessageList({MessageType? type}) async {
// 模拟网络延迟
await Future.delayed(const Duration(seconds: 1));
final allMessages = [
MessageCategoryModel(
type: 'alarm',
icon: 'notifications',
iconColor: '#FF3B30',
title: '告警通知',
content: '逆变器 INV-001 离网告警',
time: '09:24',
unreadCount: 12,
targetId: 'ALM-001',
targetRoute: '/alarm-detail',
),
MessageCategoryModel(
type: 'workorder',
icon: 'assignment',
iconColor: '#FF9500',
title: '工单提醒',
content: '工单 #WO-2025051908 已派发',
time: '09:18',
unreadCount: 3,
targetId: 'WO-2025051908',
targetRoute: '/workorder-detail',
),
MessageCategoryModel(
type: 'system',
icon: 'campaign',
iconColor: '#165DFF',
title: '系统消息',
content: '系统升级维护通知',
time: '08:30',
unreadCount: 0,
targetId: null,
targetRoute: null,
),
MessageCategoryModel(
type: 'manager',
icon: 'chat',
iconColor: '#00B42A',
title: '负责人消息',
content: '张工:请尽快处理该告警',
time: '08:15',
unreadCount: 2,
targetId: null,
targetRoute: null,
),
MessageCategoryModel(
type: 'daily',
icon: 'description',
iconColor: '#722ED1',
title: '日报推送',
content: '5月18日光伏电站运行日报',
time: '07:30',
unreadCount: 0,
targetId: null,
targetRoute: null,
),
MessageCategoryModel(
type: 'safety',
icon: 'security',
iconColor: '#165DFF',
title: '安全通知',
content: '安全策略更新通知',
time: '昨日',
unreadCount: 0,
targetId: null,
targetRoute: null,
),
];
// 根据类型筛选
if (type != null && type != MessageType.all) {
return allMessages
.where((msg) => msg.type == type.name)
.toList();
}
return allMessages;
}
@override
Future<bool> markAllAsRead() async {
// 模拟网络延迟
await Future.delayed(const Duration(milliseconds: 500));
return true;
}
}

View File

@@ -0,0 +1,11 @@
import 'package:maibu_satabot_v2/features/v2/message_center/data/models/message_category_model.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
/// 消息中心远程数据源抽象
abstract class MessageCenterRemoteDataSource {
/// 获取消息列表
Future<List<MessageCategoryModel>> getMessageList({MessageType? type});
/// 一键已读
Future<bool> markAllAsRead();
}

View File

@@ -0,0 +1,73 @@
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
/// 消息分类数据模型
class MessageCategoryModel {
MessageCategoryModel({
required this.type,
required this.icon,
required this.iconColor,
required this.title,
required this.content,
required this.time,
this.unreadCount = 0,
this.targetId,
this.targetRoute,
});
final String type;
final String icon;
final String iconColor;
final String title;
final String content;
final String time;
final int unreadCount;
final String? targetId;
final String? targetRoute;
factory MessageCategoryModel.fromJson(Map<String, dynamic> json) {
return MessageCategoryModel(
type: json['type'] as String,
icon: json['icon'] as String,
iconColor: json['iconColor'] as String,
title: json['title'] as String,
content: json['content'] as String,
time: json['time'] as String,
unreadCount: json['unreadCount'] as int? ?? 0,
targetId: json['targetId'] as String?,
targetRoute: json['targetRoute'] as String?,
);
}
MessageCategoryEntity toEntity() {
return MessageCategoryEntity(
type: _parseMessageType(type),
icon: icon,
iconColor: iconColor,
title: title,
content: content,
time: time,
unreadCount: unreadCount,
targetId: targetId,
targetRoute: targetRoute,
);
}
MessageType _parseMessageType(String type) {
switch (type) {
case 'alarm':
return MessageType.alarm;
case 'workorder':
return MessageType.workorder;
case 'system':
return MessageType.system;
case 'manager':
return MessageType.manager;
case 'daily':
return MessageType.daily;
case 'safety':
return MessageType.safety;
default:
return MessageType.all;
}
}
}

View File

@@ -0,0 +1,34 @@
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/data/datasources/message_center_remote_datasource.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/repositories/message_center_repository.dart';
/// 消息中心仓储实现
class MessageCenterRepositoryImpl implements MessageCenterRepository {
MessageCenterRepositoryImpl(this._remoteDataSource);
final MessageCenterRemoteDataSource _remoteDataSource;
@override
Future<Either<Failure, List<MessageCategoryEntity>>> getMessageList({
MessageType? type,
}) async {
try {
final models = await _remoteDataSource.getMessageList(type: type);
return Right(models.map((model) => model.toEntity()).toList());
} catch (e) {
return Left(ServerFailure('获取消息列表失败:$e'));
}
}
@override
Future<Either<Failure, bool>> markAllAsRead() async {
try {
final success = await _remoteDataSource.markAllAsRead();
return Right(success);
} catch (e) {
return Left(ServerFailure('标记已读失败:$e'));
}
}
}

View File

@@ -0,0 +1,94 @@
import 'package:equatable/equatable.dart';
/// 消息类型枚举
enum MessageType {
all('全部'),
alarm('告警'),
workorder('工单'),
system('系统'),
manager('负责人'),
daily('日报'),
safety('安全');
const MessageType(this.label);
final String label;
}
/// 消息分类实体
class MessageCategoryEntity extends Equatable {
const MessageCategoryEntity({
required this.type,
required this.icon,
required this.iconColor,
required this.title,
required this.content,
required this.time,
this.unreadCount = 0,
this.targetId,
this.targetRoute,
});
/// 消息类型
final MessageType type;
/// 图标名称
final String icon;
/// 图标颜色
final String iconColor;
/// 标题
final String title;
/// 内容摘要
final String content;
/// 时间
final String time;
/// 未读数量
final int unreadCount;
/// 目标ID(用于跳转)
final String? targetId;
/// 目标路由(用于跳转)
final String? targetRoute;
@override
List<Object?> get props => [
type,
icon,
iconColor,
title,
content,
time,
unreadCount,
targetId,
targetRoute,
];
MessageCategoryEntity copyWith({
MessageType? type,
String? icon,
String? iconColor,
String? title,
String? content,
String? time,
int? unreadCount,
String? targetId,
String? targetRoute,
}) {
return MessageCategoryEntity(
type: type ?? this.type,
icon: icon ?? this.icon,
iconColor: iconColor ?? this.iconColor,
title: title ?? this.title,
content: content ?? this.content,
time: time ?? this.time,
unreadCount: unreadCount ?? this.unreadCount,
targetId: targetId ?? this.targetId,
targetRoute: targetRoute ?? this.targetRoute,
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
/// 消息中心仓储抽象
abstract class MessageCenterRepository {
/// 获取消息列表
Future<Either<Failure, List<MessageCategoryEntity>>> getMessageList({
MessageType? type,
});
/// 一键已读
Future<Either<Failure, bool>> markAllAsRead();
}

View File

@@ -0,0 +1,28 @@
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/repositories/message_center_repository.dart';
/// 获取消息列表用例
class GetMessageListUseCase {
GetMessageListUseCase(this._repository);
final MessageCenterRepository _repository;
Future<Either<Failure, List<MessageCategoryEntity>>> call({
MessageType? type,
}) async {
return await _repository.getMessageList(type: type);
}
}
/// 一键已读用例
class MarkAllAsReadUseCase {
MarkAllAsReadUseCase(this._repository);
final MessageCenterRepository _repository;
Future<Either<Failure, bool>> call() async {
return await _repository.markAllAsRead();
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/usecases/message_center_usecase.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/presentation/bloc/message_center_state.dart';
/// 消息中心Cubit
class MessageCenterCubit extends Cubit<MessageCenterState> {
MessageCenterCubit({
required GetMessageListUseCase getMessageListUseCase,
required MarkAllAsReadUseCase markAllAsReadUseCase,
}) : _getMessageListUseCase = getMessageListUseCase,
_markAllAsReadUseCase = markAllAsReadUseCase,
super(MessageCenterInitial());
final GetMessageListUseCase _getMessageListUseCase;
final MarkAllAsReadUseCase _markAllAsReadUseCase;
/// 加载消息列表
Future<void> loadMessageList({MessageType? type}) async {
emit(MessageCenterLoading());
final result = await _getMessageListUseCase(type: type);
result.fold(
(failure) => emit(MessageCenterError(failure.message)),
(messageList) {
final totalUnread = messageList.fold<int>(
0,
(sum, msg) => sum + msg.unreadCount,
);
emit(MessageCenterLoaded(
messageList: messageList,
selectedType: type ?? MessageType.all,
totalUnread: totalUnread,
));
},
);
}
/// 切换消息类型筛选
Future<void> changeFilter(MessageType type) async {
await loadMessageList(type: type);
}
/// 一键已读
Future<void> markAllAsRead() async {
// 检查当前状态
if (state is! MessageCenterLoaded) return;
emit(const MessageCenterActionInProgress('一键已读'));
final result = await _markAllAsReadUseCase();
result.fold(
(failure) => emit(MessageCenterError(failure.message)),
(success) {
if (success) {
// 将所有消息的未读数清零
final currentState = state as MessageCenterLoaded;
final updatedList = currentState.messageList
.map((msg) => msg.copyWith(unreadCount: 0))
.toList();
emit(currentState.copyWith(
messageList: updatedList,
totalUnread: 0,
));
}
},
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:equatable/equatable.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
/// 消息中心状态
sealed class MessageCenterState extends Equatable {
const MessageCenterState();
@override
List<Object?> get props => [];
}
/// 初始状态
final class MessageCenterInitial extends MessageCenterState {}
/// 加载状态
final class MessageCenterLoading extends MessageCenterState {}
/// 加载成功状态
final class MessageCenterLoaded extends MessageCenterState {
const MessageCenterLoaded({
required this.messageList,
this.selectedType = MessageType.all,
this.totalUnread = 0,
});
final List<MessageCategoryEntity> messageList;
final MessageType selectedType;
final int totalUnread;
@override
List<Object?> get props => [messageList, selectedType, totalUnread];
MessageCenterLoaded copyWith({
List<MessageCategoryEntity>? messageList,
MessageType? selectedType,
int? totalUnread,
}) {
return MessageCenterLoaded(
messageList: messageList ?? this.messageList,
selectedType: selectedType ?? this.selectedType,
totalUnread: totalUnread ?? this.totalUnread,
);
}
}
/// 加载失败状态
final class MessageCenterError extends MessageCenterState {
const MessageCenterError(this.message);
final String message;
@override
List<Object?> get props => [message];
}
/// 操作中状态(一键已读)
final class MessageCenterActionInProgress extends MessageCenterState {
const MessageCenterActionInProgress(this.action);
final String action;
@override
List<Object?> get props => [action];
}

View File

@@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/di/injection.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/domain/entities/message_category_entity.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/presentation/bloc/message_center_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/message_center/presentation/bloc/message_center_state.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
/// 消息中心页面
class MessageCenterPage extends StatelessWidget {
const MessageCenterPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => sl<MessageCenterCubit>()..loadMessageList(),
child: const MessageCenterView(),
);
}
}
class MessageCenterView extends StatelessWidget {
const MessageCenterView({super.key});
@override
Widget build(BuildContext context) {
final cubit = context.read<MessageCenterCubit>();
return Scaffold(
backgroundColor: AlarmColors.background,
body: BlocConsumer<MessageCenterCubit, MessageCenterState>(
listener: (context, state) {
if (state is MessageCenterActionInProgress) {
// 显示加载提示
}
if (state is MessageCenterError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
builder: (context, state) {
return SafeArea(
child: Column(
children: [
// 顶部导航栏
_buildAppBar(context, cubit),
// 内容区域
Expanded(
child: _buildContent(context, state),
),
],
),
);
},
),
);
}
Widget _buildAppBar(BuildContext context, MessageCenterCubit cubit) {
return Container(
height: 44.0,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
// 左侧:下拉筛选菜单
_buildFilterDropdown(context, cubit),
const Spacer(),
// 右侧:一键已读按钮
GestureDetector(
onTap: () => cubit.markAllAsRead(),
child: const Text(
'一键已读',
style: TextStyle(
fontSize: 14,
color: AlarmColors.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Widget _buildFilterDropdown(BuildContext context, MessageCenterCubit cubit) {
return BlocBuilder<MessageCenterCubit, MessageCenterState>(
builder: (context, state) {
MessageType selectedType = MessageType.all;
if (state is MessageCenterLoaded) {
selectedType = state.selectedType;
}
return DropdownButton<MessageType>(
value: selectedType,
underline: const SizedBox(),
icon: const Icon(
Icons.keyboard_arrow_down,
size: 20,
color: AlarmColors.textSecondary,
),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AlarmColors.textPrimary,
),
items: MessageType.values.map((type) {
return DropdownMenuItem<MessageType>(
value: type,
child: Text(type.label),
);
}).toList(),
onChanged: (value) {
if (value != null) {
cubit.changeFilter(value);
}
},
);
},
);
}
Widget _buildContent(BuildContext context, MessageCenterState state) {
if (state is MessageCenterLoading) {
return _buildSkeletonScreen();
}
if (state is MessageCenterError) {
return _buildErrorState(context, state.message);
}
if (state is MessageCenterLoaded) {
return _buildMessageList(context, state);
}
if (state is MessageCenterActionInProgress) {
return _buildLoadingOverlay(context, state.action);
}
return const SizedBox();
}
Widget _buildMessageList(BuildContext context, MessageCenterLoaded state) {
if (state.messageList.isEmpty) {
return Center(
child: Text(
'暂无消息',
style: const TextStyle(
fontSize: 14,
color: AlarmColors.textSecondary,
),
),
);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
itemCount: state.messageList.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final message = state.messageList[index];
return _MessageCategoryCard(
message: message,
onTap: () => _handleMessageTap(context, message),
);
},
);
}
void _handleMessageTap(BuildContext context, MessageCategoryEntity message) {
// 根据目标路由跳转
if (message.targetRoute != null) {
context.push(message.targetRoute!);
} else {
// 显示消息详情(可以后续扩展)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('查看${message.title}详情')),
);
}
}
Widget _buildSkeletonScreen() {
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
itemCount: 6,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 80,
decoration: BoxDecoration(
color: AlarmColors.background,
borderRadius: BorderRadius.circular(AlarmDimensions.borderRadius),
),
child: const Center(
child: CircularProgressIndicator(),
),
);
},
);
}
Widget _buildErrorState(BuildContext context, String message) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: AlarmColors.danger,
),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
fontSize: 14,
color: AlarmColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
context.read<MessageCenterCubit>().loadMessageList();
},
child: const Text('重试'),
),
],
),
);
}
Widget _buildLoadingOverlay(BuildContext context, String action) {
return Stack(
children: [
BlocBuilder<MessageCenterCubit, MessageCenterState>(
builder: (context, state) {
if (state is MessageCenterLoaded) {
return _buildMessageList(context, state);
}
return const SizedBox();
},
),
Container(
color: Colors.black.withOpacity(0.3),
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
'$action中...',
style: const TextStyle(
fontSize: 14,
color: AlarmColors.textSecondary,
),
),
],
),
),
),
),
],
);
}
}
/// 消息分类卡片组件
class _MessageCategoryCard extends StatelessWidget {
const _MessageCategoryCard({
required this.message,
required this.onTap,
});
final MessageCategoryEntity message;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final isUnread = message.unreadCount > 0;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AlarmColors.background,
borderRadius: BorderRadius.circular(AlarmDimensions.borderRadius),
),
child: Row(
children: [
// 左侧图标
_buildIcon(),
const SizedBox(width: 12),
// 中间内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
message.title,
style: TextStyle(
fontSize: 15,
fontWeight: isUnread ? FontWeight.bold : FontWeight.w500,
color: AlarmColors.textPrimary,
),
),
const SizedBox(height: 4),
// 内容摘要
Text(
message.content,
style: const TextStyle(
fontSize: 13,
color: AlarmColors.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 12),
// 右侧信息
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// 未读角标
if (isUnread) _buildUnreadBadge(),
// 时间
Text(
message.time,
style: const TextStyle(
fontSize: 12,
color: AlarmColors.textSecondary,
),
),
],
),
],
),
),
);
}
Widget _buildIcon() {
final iconColor = Color(
int.parse(message.iconColor.replaceAll('#', '0xFF')),
);
IconData iconData;
switch (message.icon) {
case 'notifications':
iconData = Icons.notifications;
break;
case 'assignment':
iconData = Icons.assignment;
break;
case 'campaign':
iconData = Icons.campaign;
break;
case 'chat':
iconData = Icons.chat_bubble;
break;
case 'description':
iconData = Icons.description;
break;
case 'security':
iconData = Icons.security;
break;
default:
iconData = Icons.notifications;
}
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
iconData,
color: iconColor,
size: 20,
),
);
}
Widget _buildUnreadBadge() {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AlarmColors.danger,
borderRadius: BorderRadius.circular(10),
),
child: Text(
message.unreadCount.toString(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}
}

View File

@@ -0,0 +1,279 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
import 'package:maibu_satabot_v2/core/app/app_user_state.dart';
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
/// 个人信息详情页
class ProfileDetailPage extends StatelessWidget {
const ProfileDetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'个人信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
centerTitle: true,
),
body: BlocBuilder<AppUserCubit, AppUserState>(
builder: (context, state) {
final user = state.user;
if (user == null) {
return const Center(
child: Text('未登录'),
);
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
// 头像卡片
_buildAvatarCard(user),
const SizedBox(height: 16),
// 信息列表
_buildInfoCard(user),
const SizedBox(height: 16),
// 操作按钮
_buildActionButtons(context),
],
);
},
),
);
}
/// 头像卡片
Widget _buildAvatarCard(UserEntity user) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0x0D000000),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
// 头像
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF165DFF),
width: 3,
),
),
child: ClipOval(
child: _buildAvatar(user.avatar),
),
),
const SizedBox(height: 16),
// 昵称
Text(
user.nickname,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 8),
// 用户名
Text(
'@${user.username}',
style: TextStyle(
fontSize: 14,
color: const Color(0xFF86909C),
),
),
],
),
);
}
/// 构建头像
Widget _buildAvatar(String? avatarUrl) {
if (avatarUrl != null && avatarUrl.isNotEmpty) {
return Image.network(
avatarUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultAvatar();
},
);
}
return _buildDefaultAvatar();
}
/// 默认头像
Widget _buildDefaultAvatar() {
return Container(
color: const Color(0xFFF5F7FA),
child: const Icon(
Icons.person,
size: 50,
color: Color(0xFF165DFF),
),
);
}
/// 信息卡片
Widget _buildInfoCard(UserEntity user) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0x0D000000),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildInfoItem(
icon: Icons.person_outline,
label: '用户ID',
value: user.userId,
showDivider: true,
),
_buildInfoItem(
icon: Icons.account_circle_outlined,
label: '用户名',
value: user.username,
showDivider: true,
),
_buildInfoItem(
icon: Icons.badge_outlined,
label: '昵称',
value: user.nickname,
showDivider: user.email != null || user.phone != null,
),
if (user.email != null)
_buildInfoItem(
icon: Icons.email_outlined,
label: '邮箱',
value: user.email!,
showDivider: user.phone != null,
),
if (user.phone != null)
_buildInfoItem(
icon: Icons.phone_outlined,
label: '手机号',
value: user.phone!,
showDivider: false,
),
],
),
);
}
/// 信息项
Widget _buildInfoItem({
required IconData icon,
required String label,
required String value,
required bool showDivider,
}) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(icon, size: 22, color: const Color(0xFF86909C)),
const SizedBox(width: 12),
Text(
label,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF86909C),
),
),
const Spacer(),
Expanded(
flex: 2,
child: Text(
value,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1D2129),
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (showDivider)
Divider(
height: 1,
thickness: 1,
color: const Color(0xFFF2F3F5),
),
],
);
}
/// 操作按钮
Widget _buildActionButtons(BuildContext context) {
return Column(
children: [
// 编辑资料按钮(预留)
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
onPressed: () {
// TODO: 跳转到编辑资料页面
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('编辑功能开发中...')),
);
},
icon: const Icon(Icons.edit, size: 20),
label: const Text(
'编辑资料',
style: TextStyle(fontSize: 16),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,66 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../home/domain/entities/site_entity.dart';
class SiteState {
final List<SiteEntity> sites;
final SiteEntity? selectedSite;
const SiteState({
this.sites = const [],
this.selectedSite,
});
SiteState copyWith({
List<SiteEntity>? sites,
SiteEntity? selectedSite,
}) {
return SiteState(
sites: sites ?? this.sites,
selectedSite: selectedSite ?? this.selectedSite,
);
}
}
class SiteCubit extends Cubit<SiteState> {
final SharedPreferences sharedPreferences;
static const String _selectedSiteIdKey = 'selected_site_id';
SiteCubit(this.sharedPreferences) : super(const SiteState());
/// 更新场站列表
void updateSites(List<SiteEntity> sites) {
// 尝试恢复之前选中的场站
final savedSiteId = sharedPreferences.getInt(_selectedSiteIdKey);
SiteEntity? selectedSite;
if (savedSiteId != null && sites.isNotEmpty) {
selectedSite = sites.firstWhere(
(site) => site.id == savedSiteId,
orElse: () => sites.first,
);
} else if (sites.isNotEmpty) {
selectedSite = sites.first;
}
emit(state.copyWith(sites: sites, selectedSite: selectedSite));
}
/// 选择场站(持久化)
void selectSite(SiteEntity site) {
sharedPreferences.setInt(_selectedSiteIdKey, site.id);
emit(state.copyWith(selectedSite: site));
}
/// 清除选中场站
void clearSelectedSite() {
sharedPreferences.remove(_selectedSiteIdKey);
emit(state.copyWith(selectedSite: null));
}
/// 清空所有场站数据(退出登录时调用)
void clearAll() {
sharedPreferences.remove(_selectedSiteIdKey);
emit(const SiteState());
}
}

View File

@@ -0,0 +1,13 @@
import 'package:maibu_satabot_v2/features/v2/waring_center/data/models/alarm_detail_model.dart';
/// 告警详情远程数据源抽象
abstract class AlarmDetailRemoteDataSource {
/// 获取告警详情
Future<AlarmDetailModel> getAlarmDetail(String alarmId);
/// 确认告警
Future<bool> confirmAlarm(String alarmId);
/// AI诊断
Future<String> aiDiagnosis(String alarmId);
}

View File

@@ -0,0 +1,55 @@
import 'package:maibu_satabot_v2/features/v2/waring_center/data/datasources/alarm_detail_remote_datasource.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/data/models/alarm_detail_model.dart';
/// 告警详情远程数据源实现(模拟数据)
class AlarmDetailRemoteDataSourceImpl implements AlarmDetailRemoteDataSource {
@override
Future<AlarmDetailModel> getAlarmDetail(String alarmId) async {
// 模拟网络延迟
await Future.delayed(const Duration(seconds: 1));
return AlarmDetailModel(
id: alarmId,
title: '逆变器离网告警',
level: '严重',
deviceInfo: '光伏区A / 逆变器 INV-001',
occurTime: '2025-05-19T09:24:30',
alarmStatus: '未处理',
recoverTime: null,
duration: '36分钟',
affectRange: '光伏区A(2.6 MW)',
description: '逆变器与电网失去连接,功率输出为0。',
suggestions: [
'检查逆变器侧并网开关状态',
'检查电网电压及频率是否异常',
],
historyData: HistoryDataModel(
power: [
MetricPointModel(time: '08:24', value: 1050),
MetricPointModel(time: '08:54', value: 980),
MetricPointModel(time: '09:04', value: 1020),
MetricPointModel(time: '09:14', value: 990),
MetricPointModel(time: '09:19', value: 1010),
MetricPointModel(time: '09:24', value: 100),
MetricPointModel(time: '09:24:30', value: 0),
],
voltage: [],
frequency: [],
),
);
}
@override
Future<bool> confirmAlarm(String alarmId) async {
// 模拟网络延迟
await Future.delayed(const Duration(seconds: 1));
return true;
}
@override
Future<String> aiDiagnosis(String alarmId) async {
// 模拟网络延迟
await Future.delayed(const Duration(seconds: 2));
return 'AI分析结果:\n\n根据历史数据分析,该告警可能是由于以下原因导致:\n1. 电网侧电压波动超过阈值\n2. 逆变器保护机制触发\n3. 并网开关异常断开\n\n建议优先检查并网开关状态和电网电压稳定性。';
}
}

View File

@@ -0,0 +1,152 @@
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_detail_entity.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
/// 告警详情数据模型
class AlarmDetailModel {
AlarmDetailModel({
required this.id,
required this.title,
required this.level,
required this.deviceInfo,
required this.occurTime,
required this.alarmStatus,
this.recoverTime,
required this.duration,
required this.affectRange,
required this.description,
required this.suggestions,
required this.historyData,
});
final String id;
final String title;
final String level;
final String deviceInfo;
final String occurTime;
final String alarmStatus;
final String? recoverTime;
final String duration;
final String affectRange;
final String description;
final List<String> suggestions;
final HistoryDataModel historyData;
factory AlarmDetailModel.fromJson(Map<String, dynamic> json) {
return AlarmDetailModel(
id: json['id'] as String,
title: json['title'] as String,
level: json['level'] as String,
deviceInfo: json['deviceInfo'] as String,
occurTime: json['occurTime'] as String,
alarmStatus: json['alarmStatus'] as String,
recoverTime: json['recoverTime'] as String?,
duration: json['duration'] as String,
affectRange: json['affectRange'] as String,
description: json['description'] as String,
suggestions: (json['suggestions'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
historyData: HistoryDataModel.fromJson(
json['historyData'] as Map<String, dynamic>,
),
);
}
AlarmDetailEntity toEntity() {
return AlarmDetailEntity(
id: id,
title: title,
level: _parseLevel(level),
deviceInfo: deviceInfo,
occurTime: occurTime,
alarmStatus: alarmStatus,
recoverTime: recoverTime,
duration: duration,
affectRange: affectRange,
description: description,
suggestions: suggestions,
historyData: historyData.toEntity(),
);
}
AlarmLevel _parseLevel(String level) {
switch (level) {
case '严重':
return AlarmLevel.danger;
case '高危':
return AlarmLevel.danger;
case '中危':
return AlarmLevel.warning;
case '低危':
return AlarmLevel.low;
case '提示':
return AlarmLevel.info;
default:
return AlarmLevel.info;
}
}
}
/// 历史数据模型
class HistoryDataModel {
HistoryDataModel({
this.power = const [],
this.voltage = const [],
this.frequency = const [],
});
final List<MetricPointModel> power;
final List<MetricPointModel> voltage;
final List<MetricPointModel> frequency;
factory HistoryDataModel.fromJson(Map<String, dynamic> json) {
return HistoryDataModel(
power: (json['power'] as List<dynamic>?)
?.map((e) => MetricPointModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
voltage: (json['voltage'] as List<dynamic>?)
?.map((e) => MetricPointModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
frequency: (json['frequency'] as List<dynamic>?)
?.map((e) => MetricPointModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
HistoryMetrics toEntity() {
return HistoryMetrics(
power: power.map((e) => e.toEntity()).toList(),
voltage: voltage.map((e) => e.toEntity()).toList(),
frequency: frequency.map((e) => e.toEntity()).toList(),
);
}
}
/// 指标点模型
class MetricPointModel {
MetricPointModel({
required this.time,
required this.value,
});
final String time;
final num value;
factory MetricPointModel.fromJson(Map<String, dynamic> json) {
return MetricPointModel(
time: json['time'] as String,
value: json['value'] as num,
);
}
MetricDataPoint toEntity() {
return MetricDataPoint(
time: time,
value: value.toDouble(),
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/data/datasources/alarm_detail_remote_datasource.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_detail_entity.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/repositories/alarm_detail_repository.dart';
/// 告警详情仓储实现
class AlarmDetailRepositoryImpl implements AlarmDetailRepository {
AlarmDetailRepositoryImpl(this._remoteDataSource);
final AlarmDetailRemoteDataSource _remoteDataSource;
@override
Future<Either<Failure, AlarmDetailEntity>> getAlarmDetail(String alarmId) async {
try {
final model = await _remoteDataSource.getAlarmDetail(alarmId);
return right(model.toEntity());
} catch (e) {
return left(ServerFailure('获取告警详情失败: $e'));
}
}
@override
Future<Either<Failure, bool>> confirmAlarm(String alarmId) async {
try {
final result = await _remoteDataSource.confirmAlarm(alarmId);
return right(result);
} catch (e) {
return left(ServerFailure('确认告警失败: $e'));
}
}
@override
Future<Either<Failure, String>> aiDiagnosis(String alarmId) async {
try {
final result = await _remoteDataSource.aiDiagnosis(alarmId);
return right(result);
} catch (e) {
return left(ServerFailure('AI诊断失败: $e'));
}
}
}

View File

@@ -0,0 +1,132 @@
import 'package:equatable/equatable.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
/// 指标数据点
class MetricDataPoint extends Equatable {
const MetricDataPoint({
required this.time,
required this.value,
});
final String time;
final double value;
@override
List<Object?> get props => [time, value];
}
/// 历史指标数据
class HistoryMetrics extends Equatable {
const HistoryMetrics({
this.power = const [],
this.voltage = const [],
this.frequency = const [],
});
final List<MetricDataPoint> power;
final List<MetricDataPoint> voltage;
final List<MetricDataPoint> frequency;
@override
List<Object?> get props => [power, voltage, frequency];
}
/// 告警详情实体
class AlarmDetailEntity extends Equatable {
const AlarmDetailEntity({
required this.id,
required this.title,
required this.level,
required this.deviceInfo,
required this.occurTime,
required this.alarmStatus,
required this.recoverTime,
required this.duration,
required this.affectRange,
required this.description,
required this.suggestions,
required this.historyData,
});
/// 告警ID
final String id;
/// 告警标题
final String title;
/// 告警等级
final AlarmLevel level;
/// 设备信息
final String deviceInfo;
/// 发生时间
final String occurTime;
/// 告警状态
final String alarmStatus;
/// 恢复时间
final String? recoverTime;
/// 持续时长
final String duration;
/// 影响范围
final String affectRange;
/// 告警描述
final String description;
/// 处理建议
final List<String> suggestions;
/// 历史指标数据
final HistoryMetrics historyData;
@override
List<Object?> get props => [
id,
title,
level,
deviceInfo,
occurTime,
alarmStatus,
recoverTime,
duration,
affectRange,
description,
suggestions,
historyData,
];
AlarmDetailEntity copyWith({
String? id,
String? title,
AlarmLevel? level,
String? deviceInfo,
String? occurTime,
String? alarmStatus,
String? recoverTime,
String? duration,
String? affectRange,
String? description,
List<String>? suggestions,
HistoryMetrics? historyData,
}) {
return AlarmDetailEntity(
id: id ?? this.id,
title: title ?? this.title,
level: level ?? this.level,
deviceInfo: deviceInfo ?? this.deviceInfo,
occurTime: occurTime ?? this.occurTime,
alarmStatus: alarmStatus ?? this.alarmStatus,
recoverTime: recoverTime ?? this.recoverTime,
duration: duration ?? this.duration,
affectRange: affectRange ?? this.affectRange,
description: description ?? this.description,
suggestions: suggestions ?? this.suggestions,
historyData: historyData ?? this.historyData,
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_detail_entity.dart';
/// 告警详情仓储抽象
abstract class AlarmDetailRepository {
/// 获取告警详情
Future<Either<Failure, AlarmDetailEntity>> getAlarmDetail(String alarmId);
/// 确认告警
Future<Either<Failure, bool>> confirmAlarm(String alarmId);
/// AI诊断
Future<Either<Failure, String>> aiDiagnosis(String alarmId);
}

View File

@@ -0,0 +1,25 @@
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/repositories/alarm_detail_repository.dart';
/// 确认告警用例
class ConfirmAlarmUseCase {
ConfirmAlarmUseCase(this._repository);
final AlarmDetailRepository _repository;
Future<Either<Failure, bool>> call(String alarmId) async {
return await _repository.confirmAlarm(alarmId);
}
}
/// AI诊断用例
class AIDiagnosisUseCase {
AIDiagnosisUseCase(this._repository);
final AlarmDetailRepository _repository;
Future<Either<Failure, String>> call(String alarmId) async {
return await _repository.aiDiagnosis(alarmId);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_detail_entity.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/repositories/alarm_detail_repository.dart';
/// 获取告警详情用例
class GetAlarmDetailUseCase {
GetAlarmDetailUseCase(this._repository);
final AlarmDetailRepository _repository;
Future<Either<Failure, AlarmDetailEntity>> call(String alarmId) async {
return await _repository.getAlarmDetail(alarmId);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/usecases/alarm_actions_usecase.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/usecases/get_alarm_detail_usecase.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_detail_state.dart';
/// 告警详情 Cubit
class AlarmDetailCubit extends Cubit<AlarmDetailState> {
AlarmDetailCubit({
required GetAlarmDetailUseCase getAlarmDetailUseCase,
required ConfirmAlarmUseCase confirmAlarmUseCase,
required AIDiagnosisUseCase aiDiagnosisUseCase,
}) : _getAlarmDetailUseCase = getAlarmDetailUseCase,
_confirmAlarmUseCase = confirmAlarmUseCase,
_aiDiagnosisUseCase = aiDiagnosisUseCase,
super(AlarmDetailInitial());
final GetAlarmDetailUseCase _getAlarmDetailUseCase;
final ConfirmAlarmUseCase _confirmAlarmUseCase;
final AIDiagnosisUseCase _aiDiagnosisUseCase;
/// 加载告警详情
Future<void> loadAlarmDetail(String alarmId) async {
emit(AlarmDetailLoading());
try {
final result = await _getAlarmDetailUseCase(alarmId);
result.fold(
(failure) => emit(AlarmDetailError(failure.message)),
(alarmDetail) => emit(AlarmDetailLoaded(alarmDetail: alarmDetail)),
);
} catch (e) {
emit(AlarmDetailError('加载告警详情失败: $e'));
}
}
/// 切换指标类型
void changeMetricType(MetricType metricType) {
final currentState = state;
if (currentState is AlarmDetailLoaded) {
emit(currentState.copyWith(selectedMetric: metricType));
}
}
/// 确认告警
Future<void> confirmAlarm(String alarmId) async {
final currentState = state;
if (currentState is! AlarmDetailLoaded) return;
emit(const AlarmDetailActionInProgress('确认告警'));
try {
final result = await _confirmAlarmUseCase(alarmId);
result.fold(
(failure) {
emit(AlarmDetailError(failure.message));
},
(success) {
if (success) {
emit(
currentState.copyWith(
isConfirmed: true,
alarmDetail: currentState.alarmDetail.copyWith(
alarmStatus: '已处理',
),
),
);
}
},
);
} catch (e) {
emit(AlarmDetailError('确认告警失败: $e'));
}
}
/// AI诊断
Future<void> aiDiagnosis(String alarmId) async {
final currentState = state;
if (currentState is! AlarmDetailLoaded) return;
emit(const AlarmDetailActionInProgress('AI诊断'));
try {
final result = await _aiDiagnosisUseCase(alarmId);
result.fold(
(failure) {
emit(AlarmDetailError(failure.message));
},
(diagnosisResult) {
emit(currentState.copyWith(aiDiagnosisResult: diagnosisResult));
},
);
} catch (e) {
emit(AlarmDetailError('AI诊断失败: $e'));
}
}
}

View File

@@ -0,0 +1,78 @@
import 'package:equatable/equatable.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_detail_entity.dart';
/// 指标类型枚举
enum MetricType {
power('功率(KW)'),
voltage('电压(V)'),
frequency('频率(Hz)');
const MetricType(this.label);
final String label;
}
/// 告警详情状态
abstract class AlarmDetailState extends Equatable {
const AlarmDetailState();
@override
List<Object?> get props => [];
}
/// 初始状态
class AlarmDetailInitial extends AlarmDetailState {}
/// 加载状态
class AlarmDetailLoading extends AlarmDetailState {}
/// 加载成功状态
class AlarmDetailLoaded extends AlarmDetailState {
const AlarmDetailLoaded({
required this.alarmDetail,
this.selectedMetric = MetricType.power,
this.isConfirmed = false,
this.aiDiagnosisResult,
});
final AlarmDetailEntity alarmDetail;
final MetricType selectedMetric;
final bool isConfirmed;
final String? aiDiagnosisResult;
@override
List<Object?> get props => [alarmDetail, selectedMetric, isConfirmed, aiDiagnosisResult];
AlarmDetailLoaded copyWith({
AlarmDetailEntity? alarmDetail,
MetricType? selectedMetric,
bool? isConfirmed,
String? aiDiagnosisResult,
}) {
return AlarmDetailLoaded(
alarmDetail: alarmDetail ?? this.alarmDetail,
selectedMetric: selectedMetric ?? this.selectedMetric,
isConfirmed: isConfirmed ?? this.isConfirmed,
aiDiagnosisResult: aiDiagnosisResult ?? this.aiDiagnosisResult,
);
}
}
/// 加载失败状态
class AlarmDetailError extends AlarmDetailState {
const AlarmDetailError(this.message);
final String message;
@override
List<Object?> get props => [message];
}
/// 操作进行中状态
class AlarmDetailActionInProgress extends AlarmDetailState {
const AlarmDetailActionInProgress(this.action);
final String action;
@override
List<Object?> get props => [action];
}

View File

@@ -0,0 +1,569 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_detail_entity.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/bloc/alarm_detail_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/widgets/metric_trend_chart.dart';
/// 告警详情页面
class AlarmDetailPage extends StatefulWidget {
final String alarmId;
const AlarmDetailPage({
super.key,
required this.alarmId,
});
@override
State<AlarmDetailPage> createState() => _AlarmDetailPageState();
}
class _AlarmDetailPageState extends State<AlarmDetailPage> {
late AlarmDetailCubit _cubit;
@override
void initState() {
super.initState();
_cubit = context.read<AlarmDetailCubit>();
_cubit.loadAlarmDetail(widget.alarmId);
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark.copyWith(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
appBar: _buildAppBar(),
body: BlocConsumer<AlarmDetailCubit, AlarmDetailState>(
listener: (context, state) {
// 监听AI诊断结果
if (state is AlarmDetailLoaded && state.aiDiagnosisResult != null) {
_showAIDiagnosisDialog(state.aiDiagnosisResult!);
}
// 确认告警成功后返回
if (state is AlarmDetailLoaded && state.isConfirmed) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('告警已确认'),
backgroundColor: AlarmColors.success,
),
);
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
context.pop();
}
});
}
},
builder: (context, state) {
if (state is AlarmDetailLoading || state is AlarmDetailInitial) {
return _buildSkeletonScreen();
}
if (state is AlarmDetailError) {
return _buildErrorState(state.message);
}
if (state is AlarmDetailLoaded) {
return _buildContent(state);
}
if (state is AlarmDetailActionInProgress) {
return _buildLoadingOverlay(state.action);
}
return const SizedBox();
},
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: AlarmColors.textPrimary),
onPressed: () => context.pop(),
),
title: const Text(
'告警详情',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AlarmColors.textPrimary,
),
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.share_outlined, color: AlarmColors.textPrimary),
onPressed: () {
// TODO: 分享功能
},
),
],
);
}
/// 骨架屏
Widget _buildSkeletonScreen() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildSkeletonCard(height: 120),
const SizedBox(height: 16),
_buildSkeletonCard(height: 200),
const SizedBox(height: 16),
_buildSkeletonCard(height: 150),
const SizedBox(height: 80),
],
),
);
}
Widget _buildSkeletonCard({required double height}) {
return Container(
height: height,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: CircularProgressIndicator(
color: AlarmColors.primary,
),
),
);
}
/// 错误状态
Widget _buildErrorState(String message) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: AlarmColors.textTertiary,
),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
fontSize: 14,
color: AlarmColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => _cubit.loadAlarmDetail(widget.alarmId),
icon: const Icon(Icons.refresh),
label: const Text('重试'),
style: ElevatedButton.styleFrom(
backgroundColor: AlarmColors.primary,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
),
),
],
),
);
}
/// 加载中覆盖层
Widget _buildLoadingOverlay(String action) {
return Container(
color: Colors.black.withOpacity(0.3),
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: AlarmColors.primary,
),
const SizedBox(height: 16),
Text(
'$action...',
style: const TextStyle(
fontSize: 14,
color: AlarmColors.textSecondary,
),
),
],
),
),
),
),
);
}
/// 主内容
Widget _buildContent(AlarmDetailLoaded state) {
final alarm = state.alarmDetail;
return Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 告警头部信息
_buildAlarmHeader(alarm),
const SizedBox(height: 16),
// 告警详情信息列表
_buildAlarmInfoList(alarm),
const SizedBox(height: 16),
// 指标趋势图
MetricTrendChart(
historyData: alarm.historyData,
selectedMetric: state.selectedMetric,
onMetricChanged: (metric) => _cubit.changeMetricType(metric),
),
const SizedBox(height: 16),
// 处理建议
_buildSuggestionsCard(alarm.suggestions),
const SizedBox(height: 100), // 为底部按钮留出空间
],
),
),
// 底部固定按钮
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildBottomButtons(state),
),
],
);
}
/// 告警头部信息
Widget _buildAlarmHeader(AlarmDetailEntity alarm) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [AlarmDimensions.cardShadow],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.warning_amber_rounded,
color: AlarmColors.danger,
size: 24,
),
const SizedBox(width: 8),
Expanded(
child: Text(
alarm.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AlarmColors.textPrimary,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AlarmColors.danger.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
alarm.level.label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AlarmColors.danger,
),
),
),
],
),
const SizedBox(height: 12),
Text(
alarm.deviceInfo,
style: const TextStyle(
fontSize: 13,
color: AlarmColors.textSecondary,
),
),
const SizedBox(height: 4),
Text(
alarm.occurTime,
style: const TextStyle(
fontSize: 13,
color: AlarmColors.textTertiary,
),
),
],
),
);
}
/// 告警详情信息列表
Widget _buildAlarmInfoList(AlarmDetailEntity alarm) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [AlarmDimensions.cardShadow],
),
child: Column(
children: [
_buildInfoRow('告警状态', alarm.alarmStatus,
valueColor: alarm.alarmStatus == '未处理' ? AlarmColors.danger : AlarmColors.success),
_buildDivider(),
_buildInfoRow('发生时间', alarm.occurTime),
_buildDivider(),
_buildInfoRow('恢复时间', alarm.recoverTime ?? '--'),
_buildDivider(),
_buildInfoRow('持续时长', alarm.duration),
_buildDivider(),
_buildInfoRow('影响范围', alarm.affectRange),
_buildDivider(),
_buildInfoRow('告警描述', alarm.description, isLast: true),
],
),
);
}
Widget _buildInfoRow(String label, String value, {Color? valueColor, bool isLast = false}) {
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 14,
color: AlarmColors.textSecondary,
),
),
const Spacer(),
Expanded(
flex: 2,
child: Text(
value,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: valueColor ?? AlarmColors.textPrimary,
),
),
),
],
),
);
}
Widget _buildDivider() {
return const Divider(
height: 24,
thickness: 0.5,
color: Color(0xFFE5E6EB),
);
}
/// 处理建议卡片
Widget _buildSuggestionsCard(List<String> suggestions) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [AlarmDimensions.cardShadow],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'处理建议',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AlarmColors.textPrimary,
),
),
const SizedBox(height: 12),
...suggestions.asMap().entries.map((entry) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${entry.key + 1}. ',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AlarmColors.textPrimary,
),
),
Expanded(
child: Text(
entry.value,
style: const TextStyle(
fontSize: 14,
color: AlarmColors.textSecondary,
height: 1.5,
),
),
),
],
),
);
}).toList(),
],
),
);
}
/// 底部按钮
Widget _buildBottomButtons(AlarmDetailLoaded state) {
final isConfirmed = state.isConfirmed;
final isInProgress = _cubit.state is AlarmDetailActionInProgress;
final isDisabled = isConfirmed || isInProgress;
return Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
// AI诊断按钮
Expanded(
child: _buildButton(
text: 'AI诊断',
isPrimary: false,
isLoading: isInProgress && _cubit.state is AlarmDetailActionInProgress &&
(_cubit.state as AlarmDetailActionInProgress).action == 'AI诊断',
isDisabled: isDisabled,
onPressed: isDisabled
? null
: () => _cubit.aiDiagnosis(widget.alarmId),
),
),
const SizedBox(width: 16),
// 确认告警按钮
Expanded(
child: _buildButton(
text: '确认告警',
isPrimary: true,
isLoading: isInProgress && _cubit.state is AlarmDetailActionInProgress &&
(_cubit.state as AlarmDetailActionInProgress).action == '确认告警',
isDisabled: isDisabled,
onPressed: isDisabled
? null
: () => _cubit.confirmAlarm(widget.alarmId),
),
),
],
),
);
}
Widget _buildButton({
required String text,
required bool isPrimary,
required bool isLoading,
required bool isDisabled,
VoidCallback? onPressed,
}) {
return SizedBox(
height: 48,
child: ElevatedButton(
onPressed: isLoading || isDisabled ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: isPrimary ? AlarmColors.primary : Colors.white,
foregroundColor: isPrimary ? Colors.white : AlarmColors.primary,
elevation: 0,
side: isPrimary ? null : const BorderSide(color: AlarmColors.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: isPrimary ? Colors.white : AlarmColors.primary,
),
)
: Text(
text,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
);
}
/// 显示AI诊断结果弹窗
void _showAIDiagnosisDialog(String result) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text(
'AI诊断结果',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
content: SingleChildScrollView(
child: Text(
result,
style: const TextStyle(
fontSize: 14,
height: 1.6,
),
),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: const Text('关闭'),
),
],
);
},
);
}
}

View File

@@ -0,0 +1,221 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_detail_entity.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_detail_state.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
/// 指标趋势图组件
class MetricTrendChart extends StatelessWidget {
final HistoryMetrics historyData;
final MetricType selectedMetric;
final Function(MetricType) onMetricChanged;
const MetricTrendChart({
super.key,
required this.historyData,
required this.selectedMetric,
required this.onMetricChanged,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [AlarmDimensions.cardShadow],
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'指标趋势',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AlarmColors.textPrimary,
),
),
const SizedBox(height: 12),
// 指标切换标签
_buildMetricTabs(),
const SizedBox(height: 16),
// 折线图
SizedBox(
height: 200,
child: _buildChart(),
),
],
),
);
}
Widget _buildMetricTabs() {
return Row(
children: MetricType.values.map((metric) {
final isSelected = metric == selectedMetric;
return Expanded(
child: GestureDetector(
onTap: () => onMetricChanged(metric),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFE8F3FF) : Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Text(
metric.label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected ? AlarmColors.primary : AlarmColors.textSecondary,
),
),
),
),
);
}).toList(),
);
}
Widget _buildChart() {
List<MetricDataPoint> dataPoints;
double maxY;
switch (selectedMetric) {
case MetricType.power:
dataPoints = historyData.power;
maxY = 1500;
break;
case MetricType.voltage:
dataPoints = historyData.voltage;
maxY = 800;
break;
case MetricType.frequency:
dataPoints = historyData.frequency;
maxY = 60;
break;
}
if (dataPoints.isEmpty) {
return const Center(
child: Text(
'暂无数据',
style: TextStyle(color: AlarmColors.textTertiary),
),
);
}
final spots = dataPoints
.asMap()
.entries
.map((e) => FlSpot(e.key.toDouble(), e.value.value))
.toList();
return LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: true,
horizontalInterval: maxY / 5,
getDrawingHorizontalLine: (value) {
return FlLine(
color: const Color(0xFFE5E6EB).withOpacity(0.3),
strokeWidth: 1,
);
},
getDrawingVerticalLine: (value) {
return FlLine(
color: const Color(0xFFE5E6EB).withOpacity(0.3),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < dataPoints.length) {
// 只显示部分标签避免拥挤
if (index == 0 || index == dataPoints.length ~/ 2 || index == dataPoints.length - 1) {
return Text(
dataPoints[index].time,
style: const TextStyle(
fontSize: 11,
color: AlarmColors.textTertiary,
),
);
}
}
return const SizedBox();
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: maxY / 5,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(
fontSize: 11,
color: AlarmColors.textTertiary,
),
);
},
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: (dataPoints.length - 1).toDouble(),
minY: 0,
maxY: maxY,
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: AlarmColors.primary,
barWidth: 2,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 3,
color: AlarmColors.danger,
strokeWidth: 1,
strokeColor: Colors.white,
);
},
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
AlarmColors.primary.withOpacity(0.2),
AlarmColors.primary.withOpacity(0.0),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
),
);
}
}

View File

@@ -12,6 +12,7 @@ import flutter_webrtc
import geolocator_apple import geolocator_apple
import isar_community_flutter_libs import isar_community_flutter_libs
import mobile_scanner import mobile_scanner
import open_file_mac
import package_info_plus import package_info_plus
import sentry_flutter import sentry_flutter
import shared_preferences_foundation import shared_preferences_foundation
@@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

35
start_mock_server.bat Normal file
View File

@@ -0,0 +1,35 @@
@echo off
echo ========================================
echo Flutter Patcher Mock Server
echo ========================================
echo.
REM 检查 dist 目录是否存在
if not exist "dist" (
echo [错误] dist 目录不存在,正在创建...
mkdir dist\version
mkdir dist\download
mkdir dist\patch
echo [完成] 目录创建成功
echo.
)
REM 检查 check.json 是否存在
if not exist "dist\version\check.json" (
echo [警告] dist\version\check.json 不存在
echo 请先配置版本检查信息
echo.
pause
exit /b 1
)
echo [提示] 正在启动 Mock Server...
echo [地址] http://127.0.0.1:8080
echo [目录] dist/
echo.
echo 按 Ctrl+C 停止服务器
echo.
dart run flutter_patcher:mock_server --dist dist
pause