Commit 9474420e authored by huqu's avatar huqu

fix bugs

parent 7566c742
......@@ -97,6 +97,8 @@ class AlertItemBO extends Equatable {
final String waitingDurationDesc;
final String alertTime;
final String alertCategory;
final String deviceCode;
final String deviceLocation;
const AlertItemBO({
required this.alertId,
......@@ -117,6 +119,8 @@ class AlertItemBO extends Equatable {
required this.waitingDurationDesc,
required this.alertTime,
required this.alertCategory,
required this.deviceCode,
required this.deviceLocation,
});
factory AlertItemBO.fromJson(Map<String, dynamic> json) {
......@@ -139,6 +143,8 @@ class AlertItemBO extends Equatable {
waitingDurationDesc: json['waitingDurationDesc'] as String? ?? '',
alertTime: json['alertTime'] as String? ?? '',
alertCategory: json['alertCategory'] as String? ?? '',
deviceCode: json['deviceCode'] as String? ?? '',
deviceLocation: json['deviceLocation'] as String? ?? '',
);
}
......@@ -162,6 +168,8 @@ class AlertItemBO extends Equatable {
waitingDurationDesc,
alertTime,
alertCategory,
deviceCode,
deviceLocation,
];
}
......@@ -181,6 +189,8 @@ class DeviceItemBO extends Equatable {
final String current;
final String onlineStatus;
final String lastHeartbeat;
final String deviceCode;
final String deviceLocation;
const DeviceItemBO({
required this.deviceId,
......@@ -196,6 +206,8 @@ class DeviceItemBO extends Equatable {
required this.current,
required this.onlineStatus,
required this.lastHeartbeat,
required this.deviceCode,
required this.deviceLocation,
});
factory DeviceItemBO.fromJson(Map<String, dynamic> json) {
......@@ -213,6 +225,8 @@ class DeviceItemBO extends Equatable {
current: json['current'] as String? ?? '',
onlineStatus: json['onlineStatus'] as String? ?? '0',
lastHeartbeat: json['lastHeartbeat'] as String? ?? '',
deviceCode: json['deviceCode'] as String? ?? '',
deviceLocation: json['deviceLocation'] as String? ?? '',
);
}
......@@ -231,5 +245,7 @@ class DeviceItemBO extends Equatable {
current,
onlineStatus,
lastHeartbeat,
deviceCode,
deviceLocation,
];
}
\ No newline at end of file
import 'package:equatable/equatable.dart';
class InspectionRoomBO extends Equatable {
final int roomId;
final String roomNumber;
final InspectionRoomRunStatusBO runStatus;
final List<InspectionRoomGroupsBO> roomGroups;
const InspectionRoomBO({
required this.roomId,
required this.roomNumber,
required this.runStatus,
required this.roomGroups,
});
factory InspectionRoomBO.fromJson(Map<String, dynamic> json) {
final roomGroups = json['roomGroups'] as List<dynamic>? ?? [];
return InspectionRoomBO(
roomId: int.tryParse(json['roomId']?.toString() ?? '') ?? 0,
roomNumber: json['roomNumber'] as String? ?? '',
runStatus: InspectionRoomRunStatusBO.fromJson(
json['runStatus'] as Map<String, dynamic>? ?? {},
),
roomGroups: roomGroups
.map((e) => InspectionRoomGroupsBO.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
@override
List<Object?> get props => [roomId, roomNumber, runStatus];
List<Object?> get props => [runStatus, roomGroups];
}
class InspectionRoomRunStatusBO extends Equatable {
......@@ -49,4 +49,50 @@ class InspectionRoomRunStatusBO extends Equatable {
@override
List<Object?> get props => [normal, warning, offline, fault];
}
class InspectionRoomGroupsBO extends Equatable {
final String areaTypeName;
final List<InspectionRoomsBO> rooms;
const InspectionRoomGroupsBO({
required this.areaTypeName,
required this.rooms,
});
factory InspectionRoomGroupsBO.fromJson(Map<String, dynamic> json) {
final rooms = json['rooms'] as List<dynamic>? ?? [];
return InspectionRoomGroupsBO(
areaTypeName: json['areaTypeName']?.toString() ?? '',
rooms: rooms
.map((e) => InspectionRoomsBO.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
@override
List<Object?> get props => [areaTypeName, rooms];
}
class InspectionRoomsBO extends Equatable {
final int roomId;
final String roomNumber;
final String areaTypeName;
const InspectionRoomsBO({
required this.roomId,
required this.roomNumber,
required this.areaTypeName,
});
factory InspectionRoomsBO.fromJson(Map<String, dynamic> json) {
return InspectionRoomsBO(
roomId: int.tryParse(json['roomId']?.toString() ?? '') ?? 0,
roomNumber: json['roomNumber']?.toString() ?? '',
areaTypeName: json['areaTypeName']?.toString() ?? '',
);
}
@override
List<Object?> get props => [roomId, roomNumber, areaTypeName];
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ import '../models/bo/alert_list_bo.dart';
class AlertListRepository {
/// 获取告警列表
/// [alertStatus] 告警状态码:0=待处理 2=已处理 3=误报,不传则查询全部
/// [alertStatus] 告警状态码:0=待处理 2=已处理 4=误报,不传则查询全部
Future<ResponseModel<AlertListBO>> getList({
required int pageSize,
required int pageNum,
......@@ -20,8 +20,7 @@ class AlertListRepository {
return DioRequest.instance.get<AlertListBO>(
'/app/alert/list',
queryParameters: params,
fromJsonT: (data) =>
AlertListBO.fromJson(data as Map<String, dynamic>),
fromJsonT: (data) => AlertListBO.fromJson(data as Map<String, dynamic>),
);
}
}
\ No newline at end of file
......@@ -4,15 +4,11 @@ import '../models/bo/inspection_room_bo.dart';
class InspectionRoomRepository {
/// 获取设备巡检房间列表
Future<ResponseModel<List<InspectionRoomBO>>> getRooms() {
return DioRequest.instance.get<List<InspectionRoomBO>>(
Future<ResponseModel<InspectionRoomBO>> getRooms() {
return DioRequest.instance.get<InspectionRoomBO>(
'/app/device/inspection/rooms',
fromJsonT: (data) {
final list = data as List<dynamic>;
return list
.map((e) => InspectionRoomBO.fromJson(e as Map<String, dynamic>))
.toList();
},
fromJsonT: (data) =>
InspectionRoomBO.fromJson(data as Map<String, dynamic>),
);
}
}
\ No newline at end of file
......@@ -7,7 +7,7 @@ class InspectionRoomService {
InspectionRoomService({required InspectionRoomRepository repository})
: _repository = repository;
Future<List<InspectionRoomBO>> getRooms() async {
Future<InspectionRoomBO> getRooms() async {
final result = await _repository.getRooms();
if (result.success && result.data != null) {
return result.data!;
......
......@@ -54,6 +54,7 @@ class AbnormalDetailView extends StatelessWidget {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator(color: Color.fromRGBO(66, 165, 245, 1.0)));
}
if (state.error != null) {
return Center(
child: Text(
......@@ -65,10 +66,12 @@ class AbnormalDetailView extends StatelessWidget {
),
);
}
final alarmInfo = state.alarmInfo;
if (alarmInfo == null) {
return const SizedBox.shrink();
}
final cubit = context.read<AbnormalCubit>();
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w),
......@@ -94,11 +97,12 @@ class AbnormalDetailView extends StatelessWidget {
Widget _buildBottomButtons(BuildContext context, AbnormalCubit cubit) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 15.h),
padding: EdgeInsets.symmetric(vertical: 20.h),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 20.h,
children: [
if (alarmStatus != AlarmStatus.processed) ...[
if (alarmStatus != AlarmStatus.processed)
BlocBuilder<AbnormalCubit, AbnormalState>(
builder: (context, state) {
final isDisabled = state.isHandling || state.isHandled;
......@@ -109,7 +113,7 @@ class AbnormalDetailView extends StatelessWidget {
color: isDisabled
? const Color.fromRGBO(73, 149, 234, 0.4)
: const Color.fromRGBO(73, 149, 234, 1),
borderRadius: BorderRadius.circular(16.r),
borderRadius: BorderRadius.circular(40.r),
),
child: TextButton(
onPressed: isDisabled
......@@ -127,7 +131,7 @@ class AbnormalDetailView extends StatelessWidget {
: Text(
state.isHandled ? '已处理' : '确认处理',
style: TextStyle(
fontSize: 28.sp,
fontSize: 36.sp,
color: const Color.fromRGBO(237, 245, 255, 1),
),
),
......@@ -135,14 +139,12 @@ class AbnormalDetailView extends StatelessWidget {
);
},
),
SizedBox(height: 20.h),
],
Container(
width: double.infinity,
height: 88.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(100, 116, 139, 0.2),
borderRadius: BorderRadius.circular(16.r),
borderRadius: BorderRadius.circular(40.r),
),
child: TextButton(
onPressed: () {
......@@ -156,15 +158,15 @@ class AbnormalDetailView extends StatelessWidget {
Text(
'设备控制',
style: TextStyle(
fontSize: 28.sp,
fontSize: 36.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
SizedBox(width: 8.w),
Image.asset(
"lib/assets/icon/right.png",
width: 24.w,
height: 21.h,
width: 40.w,
height: 24.h,
),
],
),
......
......@@ -39,8 +39,8 @@ class AbnormalCubit extends Cubit<AbnormalState> {
return AlarmInfo(
alarmType: basic.alertTitle,
deviceInfo: basic.alertDeviceLocation,
alertLevelText: basic.alertLevelText,
urgency: _mapUrgency(basic.alertLevel),
alertLevelText: basic.alertLevel,
urgency: '',
currentTemp: info.alertValue,
voltage: '',
current: '',
......@@ -60,20 +60,10 @@ class AbnormalCubit extends Cubit<AbnormalState> {
}).toList(),
alertDeviceId: basic.alertDeviceId,
alertCount: basic.alertCount,
alertIcon: basic.alertIcon,
);
}
String _mapUrgency(String level) {
switch (level) {
case 'danger':
return '高警';
case 'warning':
return '中警';
default:
return '普通';
}
}
Future<void> handleConfirm() async {
if (state.isHandling || state.isHandled) return;
emit(state.copyWith(isHandling: true));
......
......@@ -22,6 +22,7 @@ class AlarmInfo {
final List<FlSpot> temperatureSpots;
final String alertDeviceId;
final int alertCount;
final String alertIcon;
const AlarmInfo({
required this.alarmType,
......@@ -45,6 +46,7 @@ class AlarmInfo {
required this.temperatureSpots,
required this.alertDeviceId,
required this.alertCount,
required this.alertIcon,
});
}
......
......@@ -33,7 +33,9 @@ class AlarmInfoBlock extends StatelessWidget {
SizedBox(height: 25.h),
_buildInfoRow(
'告警级别',
alarmInfo.alertLevelText,
alarmInfo.alertLevelText == '3' ? '高警(Level 3)' :
alarmInfo.alertLevelText == '2' ? '中警(Level 2)' :
alarmInfo.alertLevelText == '1' ? '低警(Level 1)' : '',
valueColor: const Color.fromRGBO(255, 100, 101, 1),
),
SizedBox(height: 25.h),
......@@ -65,7 +67,6 @@ class AlarmInfoBlock extends StatelessWidget {
Widget _buildInfoRow(String label, String value, {Color? valueColor}) {
final displayValue = _truncateText(value);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
......@@ -74,12 +75,18 @@ class AlarmInfoBlock extends StatelessWidget {
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
Text(
displayValue,
style: TextStyle(
fontSize: 24.sp,
color: valueColor ?? const Color.fromRGBO(100, 116, 139, 1),
),
SizedBox(width: 20.w),
Expanded(
child: Text(
displayValue,
style: TextStyle(
fontSize: 24.sp,
color: valueColor ?? const Color.fromRGBO(100, 116, 139, 1),
),
textAlign: TextAlign.end,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
......
......@@ -18,65 +18,53 @@ class TemperatureBlock extends StatelessWidget {
color: Colors.white,
),
child: Column(
spacing: 5.h,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20.w,
children: [
Container(
width: 70.w,
height: 66.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(219, 234, 254, 1),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.thermostat_outlined,
color: const Color.fromRGBO(59, 130, 246, 1),
size: 40.sp,
),
),
SizedBox(width: 15.w),
_buildIcon(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5.h,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
alarmInfo.alarmType,
style: TextStyle(
fontSize: 28.sp,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Text(
'${alarmInfo.alertLevelText} · ',
Expanded(
child: Text(
alarmInfo.alarmType,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
fontSize: 28.sp,
color: Colors.black,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h),
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 158, 159, 0.5),
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
alarmInfo.urgency,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
),
],
),
Text(
alarmInfo.alertLevelText == '3' ? '高警' :
alarmInfo.alertLevelText == '2' ? '中警' :
alarmInfo.alertLevelText == '1' ? '低警' : '',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
if (alarmInfo.alertLevelText == '3') ...[
Container(
width: 5.w,
height: 5.h,
margin: EdgeInsets.symmetric(horizontal: 5.w),
decoration: const BoxDecoration(
color: Color.fromRGBO(255, 100, 101, 1),
shape: BoxShape.circle
),
),
_buildLevelBadge()
],
],
),
Text(
......@@ -91,21 +79,20 @@ class TemperatureBlock extends StatelessWidget {
),
],
),
SizedBox(height: 5.h),
Row(
children: [
SizedBox(width: 85.w),
Text(
'告警数量:',
style: TextStyle(
fontSize: 28.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
Text(
alarmInfo.alertCount.toString(),
style: TextStyle(
fontSize: 28.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
......@@ -115,4 +102,78 @@ class TemperatureBlock extends StatelessWidget {
)
);
}
Widget _buildIcon() {
Color? bgColor;
String icon = '';
double iconW = 0;
double iconH = 0;
switch (alarmInfo.alertIcon) {
case 'temperature'://温度
bgColor = const Color.fromRGBO(237, 245, 255, 1);
icon = 'lib/assets/icon/thermostat_bg.png';
iconW = 20.w;
iconH = 38.h;
break;
case 'voltage'://电压
bgColor = const Color.fromRGBO(255, 211, 213, 1);
icon = 'lib/assets/icon/voltage_bg.png';
iconW = 40.w;
iconH = 30.h;
break;
case 'current'://电流
bgColor = const Color.fromRGBO(255, 161, 72, 1);
icon = 'lib/assets/icon/current_bg.png';
iconW = 30.w;
iconH = 38.h;
break;
case 'alert'://漏电
bgColor = const Color.fromRGBO(255, 128, 114, 1);
icon = 'lib/assets/icon/alert_bg.png';
iconW = 46.w;
iconH = 32.h;
break;
case 'offline'://离线
bgColor = const Color.fromRGBO(229, 241, 255, 1);
icon = 'lib/assets/icon/offline_bg.png';
iconW = 44.w;
iconH = 32.h;
break;
}
return Container(
width: 64.w,
height: 53.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8.r),
),
child: icon.isNotEmpty ? Image.asset(icon, width: iconW, height: iconH) :
const SizedBox.shrink(),
);
}
Widget _buildLevelBadge() {
return Container(
width: 73.w,
height: 32.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 100, 101, 0.2),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: const Color.fromRGBO(255, 158, 159, 0.5),
width: 1.w,
)
),
child: Text(
'紧急',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
);
}
}
\ No newline at end of file
......@@ -8,8 +8,8 @@ import 'package:smart_hotel_app/services/alert_list_service.dart';
import 'package:smart_hotel_app/views/home/abnormal_list/cubit/abnormal_list_state.dart';
/// AlarmStatus → 告警状态码: 0=待处理 1=处理中 2=已确认 3=已关闭 4=误报关闭
String? alarmStatusToApi(AlarmStatus filter) {
switch (filter) {
String? alarmStatusToApi(AlarmStatus status) {
switch (status) {
case AlarmStatus.all:
return null;
case AlarmStatus.pending:
......@@ -28,8 +28,7 @@ class AbnormalListCubit extends Cubit<AbnormalListState> {
PagingController<int, AlarmItem>? _pagingController;
AbnormalListCubit()
: _service = AlertListService(
AbnormalListCubit(): _service = AlertListService(
repository: AlertListRepository(),
),
super(const AbnormalListState());
......@@ -71,12 +70,14 @@ class AbnormalListCubit extends Cubit<AbnormalListState> {
/// 切换筛选 Tab,触发重新请求
void setFilter(AlarmStatus filter) {
if (state.currentFilter == filter) return;
emit(state.copyWith(currentFilter: filter));
_pagingController?.refresh();
}
void onAlarmItemTap(AlarmItem item, BuildContext context) {
final alertId = int.tryParse(item.id) ?? 0;
context.pushRoute(AbnormalDetailRoute(alertId: alertId, alarmStatus: item.status));
context.pushRoute(AbnormalDetailRoute(
alertId: alertId, alarmStatus: item.status));
}
}
......@@ -11,7 +11,7 @@ class AlarmItem extends Equatable {
final String processTime;
final AlarmStatus status;
final String level;
final String urgency;
final String alertIcon;
const AlarmItem({
required this.id,
......@@ -21,7 +21,7 @@ class AlarmItem extends Equatable {
required this.processTime,
required this.status,
required this.level,
required this.urgency,
required this.alertIcon,
});
factory AlarmItem.fromAlertListItemBO(AlertListItemBO item) {
......@@ -32,8 +32,8 @@ class AlarmItem extends Equatable {
alarmTime: item.alertTime.isEmpty ? '--' : item.alertTime,
processTime: item.handleTime.isEmpty ? '--' : item.handleTime,
status: _mapAlertStatus(item.alertStatus),
level: _mapAlertLevel(item.alertLevel),
urgency: '',
level: item.alertLevel,
alertIcon: item.alertIcon,
);
}
......@@ -51,28 +51,9 @@ class AlarmItem extends Equatable {
}
}
static String _mapAlertLevel(String alertLevel) {
switch (alertLevel) {
case '3':
case 'danger':
return '高警';
case '2':
case 'warning':
return '中警';
case '1':
case 'info':
return '普通';
default:
return '';
}
}
@override
List<Object?> get props =>
[id, title, deviceInfo, alarmTime, processTime, status, level, urgency];
[id, title, deviceInfo, alarmTime, processTime, status, level, alertIcon];
}
class AbnormalListState extends Equatable {
......
......@@ -15,6 +15,7 @@ class AlarmListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onTap(item),
child: Container(
width: double.infinity,
......@@ -26,35 +27,52 @@ class AlarmListItem extends StatelessWidget {
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20.w,
children: [
_buildIcon(),
SizedBox(width: 20.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 10.h,
children: [
_buildTitleRow(),
SizedBox(height: 15.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.deviceInfo,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
Expanded(
child: Text(
item.deviceInfo,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 15.w),
Text(
item.level,
item.level == '3' ? '高警' :
item.level == '2' ? '中警' :
item.level == '1' ? '低警' : '',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
if (item.level == '3') ...[
Container(
width: 5.w,
height: 5.h,
margin: EdgeInsets.symmetric(horizontal: 5.w),
decoration: const BoxDecoration(
color: Color.fromRGBO(255, 100, 101, 1),
shape: BoxShape.circle
),
),
_buildLevelBadge()
],
],
),
SizedBox(height: 10.h),
Text(
'告警时间: ${item.alarmTime}',
style: TextStyle(
......@@ -62,7 +80,6 @@ class AlarmListItem extends StatelessWidget {
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
SizedBox(height: 10.h),
Text(
'处理时间: ${item.processTime}',
style: TextStyle(
......@@ -80,89 +97,128 @@ class AlarmListItem extends StatelessWidget {
}
Widget _buildIcon() {
Color? bgColor;
String icon = '';
double iconW = 0;
double iconH = 0;
switch (item.alertIcon) {
case 'temperature'://温度
bgColor = const Color.fromRGBO(237, 245, 255, 1);
icon = 'lib/assets/icon/thermostat_bg.png';
iconW = 20.w;
iconH = 38.h;
break;
case 'voltage'://电压
bgColor = const Color.fromRGBO(255, 211, 213, 1);
icon = 'lib/assets/icon/voltage_bg.png';
iconW = 40.w;
iconH = 30.h;
break;
case 'current'://电流
bgColor = const Color.fromRGBO(255, 161, 72, 1);
icon = 'lib/assets/icon/current_bg.png';
iconW = 30.w;
iconH = 38.h;
break;
case 'alert'://漏电
bgColor = const Color.fromRGBO(255, 128, 114, 1);
icon = 'lib/assets/icon/alert_bg.png';
iconW = 46.w;
iconH = 32.h;
break;
case 'offline'://离线
bgColor = const Color.fromRGBO(229, 241, 255, 1);
icon = 'lib/assets/icon/offline_bg.png';
iconW = 44.w;
iconH = 32.h;
break;
}
return Container(
width: 96.w,
height: 96.w,
width: 80.w,
height: 64.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color.fromRGBO(237, 245, 255, 1),
color: bgColor,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.thermostat,
size: 48.w,
color: const Color.fromRGBO(73, 149, 234, 1),
),
child: icon.isNotEmpty ? Image.asset(icon, width: iconW, height: iconH) :
const SizedBox.shrink(),
);
}
Widget _buildTitleRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.title,
style: TextStyle(
fontSize: 28.sp,
color: Colors.black,
fontWeight: FontWeight.bold,
Expanded(
child: Text(
item.title,
style: TextStyle(
fontSize: 28.sp,
color: Colors.black,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
_buildStatusBadge(),
// Row(
// children: [
// _buildStatusBadge(),
// SizedBox(width: 12.w),
// _buildLevelBadge(),
// SizedBox(width: 8.w),
// _buildUrgencyBadge(),
// ],
// ),
],
);
}
Widget _buildStatusBadge() {
Color bgColor;
Color textColor;
String text;
IconData? icon;
Color? bgColor;
Color? textColor;
String text = '';
String icon = '';
double iconW = 0;
double iconH = 0;
switch (item.status) {
case AlarmStatus.pending:
bgColor = const Color.fromRGBO(254, 247, 217, 1);
textColor = const Color.fromRGBO(250, 204, 21, 1);
text = '待处理';
icon = Icons.error_outline;
icon = 'lib/assets/icon/info_dcl_bg.png';
iconW = 4.w;
iconH = 15.h;
break;
case AlarmStatus.processed:
bgColor = const Color.fromRGBO(84, 214, 120, 0.15);
textColor = const Color.fromRGBO(84, 214, 120, 1);
bgColor = const Color.fromRGBO(209, 250, 229, 1);
textColor = const Color.fromRGBO(20, 184, 166, 1);
text = '已处理';
icon = Icons.check_circle_outline;
icon = 'lib/assets/icon/info_ycl_bg.png';
iconW = 18.w;
iconH = 11.h;
break;
case AlarmStatus.falseAlarm:
bgColor = const Color.fromRGBO(255, 100, 101, 0.15);
bgColor = const Color.fromRGBO(255, 224, 224, 1);
textColor = const Color.fromRGBO(255, 100, 101, 1);
text = '误报';
icon = Icons.cancel_outlined;
icon = 'lib/assets/icon/info_wb_bg.png';
iconW = 14.w;
iconH = 11.h;
break;
default:
bgColor = const Color.fromRGBO(100, 116, 139, 0.0);//完全透明
textColor = const Color.fromRGBO(100, 116, 139, 1);
text = '';
break;
}
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
width: 132.w,
height: 32.h,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(40.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 6.w,
children: [
Icon(icon, size: 30.w, color: textColor),
Image.asset(icon, width: iconW, height: iconH),
Text(
text,
style: TextStyle(
......@@ -178,35 +234,25 @@ class AlarmListItem extends StatelessWidget {
Widget _buildLevelBadge() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
width: 73.w,
height: 32.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 100, 101, 0.15),
borderRadius: BorderRadius.circular(12.r),
color: const Color.fromRGBO(255, 100, 101, 0.2),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: const Color.fromRGBO(255, 158, 159, 0.5),
width: 1.w,
)
),
child: Text(
item.level,
'紧急',
style: TextStyle(
fontSize: 22.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
);
}
Widget _buildUrgencyBadge() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 100, 101, 0.15),
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
item.urgency,
style: TextStyle(
fontSize: 22.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
);
}
}
......@@ -48,8 +48,7 @@ class DeviceCubit extends Cubit<DeviceState> {
return DeviceInfo(
deviceName: basic.deviceName,
deviceId: basic.deviceCode,
// location: basic.roomName,
deviceCode: basic.deviceCode,
location: basic.deviceLocation,
status: status,
voltage: params.voltage,
......
......@@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart';
class DeviceInfo extends Equatable {
final String deviceName;
final String deviceId;
final String deviceCode;
final String location;
final String status;
final String voltage;
......@@ -15,7 +15,7 @@ class DeviceInfo extends Equatable {
const DeviceInfo({
required this.deviceName,
required this.deviceId,
required this.deviceCode,
required this.location,
required this.status,
required this.voltage,
......@@ -30,7 +30,7 @@ class DeviceInfo extends Equatable {
@override
List<Object?> get props => [
deviceName,
deviceId,
deviceCode,
location,
status,
voltage,
......
......@@ -28,6 +28,7 @@ class PowerCharBlock extends StatelessWidget {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20.h,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
......@@ -39,25 +40,14 @@ class PowerCharBlock extends StatelessWidget {
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
color: Colors.transparent,
border: Border.all(
color: const Color.fromRGBO(73, 149, 234, 0.5),
width: 1.5.w,
),
),
child: Row(
children: [
_buildTab('功率', 0),
_buildTab('温度', 1),
],
),
Row(
children: [
_buildTab('功率', 0),
_buildTab('温度', 1),
],
),
],
),
SizedBox(height: 20.h),
SizedBox(
height: 250.h,
child: BarChart(
......@@ -93,13 +83,15 @@ class PowerCharBlock extends StatelessWidget {
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 24.sp,
fontSize: 22.sp,
),
);
},
reservedSize: 40,
reservedSize: _leftReservedSize,
),
),
bottomTitles: AxisTitles(
......@@ -143,15 +135,20 @@ class PowerCharBlock extends StatelessWidget {
Widget _buildTab(String title, int index) {
final isSelected = selectedTabIndex == index;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onTabChanged(index),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
width: 72.w,
height: 34.h,
alignment: Alignment.center,
decoration: isSelected ? BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
color: isSelected
? const Color.fromRGBO(219, 234, 254, 1)
: Colors.transparent,
),
color: const Color.fromRGBO(219, 234, 254, 1),
border: Border.all(
width: 1.w,
color: const Color.fromRGBO(73, 149, 234, 0.5),
)
) : null,
child: Text(
title,
style: TextStyle(
......@@ -236,4 +233,10 @@ class PowerCharBlock extends StatelessWidget {
return _calcMaxY(index) / 5;
}
/// 根据最大数值的位数动态计算左侧保留宽度
double get _leftReservedSize {
final maxLabel = _calcMaxY(selectedTabIndex).toInt().toString();
return maxLabel.length * 8;
}
}
\ No newline at end of file
......@@ -20,8 +20,8 @@ class StateInfoBlock extends StatelessWidget {
child: Row(
children: [
Container(
width: 80.w,
height: 70.h,
width: 64.w,
height: 53.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.r),
color: const Color.fromRGBO(219, 234, 254, 1),
......@@ -29,25 +29,25 @@ class StateInfoBlock extends StatelessWidget {
child: Icon(
Icons.power,
color: const Color.fromRGBO(59, 130, 246, 1),
size: 32.sp,
size: 38.sp,
),
),
SizedBox(width: 15.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.h,
children: [
Text(
deviceInfo.deviceName,
'${deviceInfo.location} ${deviceInfo.deviceName}',
style: TextStyle(
fontSize: 28.sp,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
Text(
deviceInfo.location,
'${deviceInfo.deviceCode}${deviceInfo.deviceName}·${deviceInfo.location}',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
......
......@@ -72,7 +72,7 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
EquipmentItem _mapDeviceToEquipment(DeviceItemBO d) {
return EquipmentItem(
icon: _mapDeviceIcon(d.deviceTypeIcon),
title: d.deviceName,
title: d.deviceLocation + d.deviceName,
subtitle: _buildDeviceSubtitle(d),
// status: d.onlineStatus == '1' ? d.runStatusText : '离线',
status: d.runStatusText,
......@@ -82,14 +82,16 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
IconData _mapDeviceIcon(String icon) {
switch (icon) {
case 'power':
return Icons.power;
case 'ac':
return Icons.air_outlined;
case 'light':
return Icons.light_outlined;
case 'tv':
return Icons.tv_outlined;
case 'curtain':
return Icons.blinds_outlined;
case 'wifi':
return Icons.wifi;
case 'thermostat':
return Icons.thermostat;
case 'light':
return Icons.light;
default:
return Icons.devices_other;
}
......
......@@ -92,7 +92,7 @@ class _HomeViewState extends State<HomeView> with AutoRouteAwareStateMixin<HomeV
List<Widget> _buildAlertBlocks(List<AlertItemBO> alerts) {
final blocks = <Widget>[];
for (final alert in alerts) {
final location = [alert.roomName, alert.deviceName]
final location = [alert.deviceLocation, alert.deviceCode + alert.deviceName]
.where((s) => s.isNotEmpty)
.join('·');
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:smart_hotel_app/utils/deal_utils.dart';
import 'package:smart_hotel_app/views/home/index/cubit/home_index_cubit.dart';
import 'package:smart_hotel_app/utils/toast_utils.dart';
......@@ -46,15 +47,13 @@ class TemperatureBlock extends StatelessWidget {
Container(
width: 66.w,
height: 54.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color.fromRGBO(235, 244, 255, 1.0),
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
Icons.thermostat_outlined,
color: const Color.fromRGBO(66, 165, 245, 1.0),
size: 40.sp,
),
child: Image.asset('lib/assets/icon/thermostat_bg.png',
width: 16.w, height: 32.h),
),
SizedBox(width: 16.w),
Expanded(
......@@ -107,7 +106,8 @@ class TemperatureBlock extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
currentTemperature.isNotEmpty ? currentTemperature : '--℃',
currentTemperature.isNotEmpty ? DealUtils.
cleanNumber(currentTemperature) : '--℃',
style: TextStyle(
fontSize: 40.sp,
fontWeight: FontWeight.bold,
......@@ -130,7 +130,8 @@ class TemperatureBlock extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
currentVoltage.isNotEmpty ? currentVoltage : '--V',
currentVoltage.isNotEmpty ?
DealUtils.cleanNumber(currentVoltage) : '--V',
style: TextStyle(
fontSize: 40.sp,
fontWeight: FontWeight.bold,
......@@ -153,7 +154,8 @@ class TemperatureBlock extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
currentCurrent.isNotEmpty ? currentCurrent : '--A',
currentCurrent.isNotEmpty ? DealUtils.
cleanNumber(currentCurrent) : '--A',
style: TextStyle(
fontSize: 40.sp,
fontWeight: FontWeight.bold,
......@@ -187,20 +189,12 @@ class TemperatureBlock extends StatelessWidget {
),
SizedBox(width: 8.w),
Text(
waitTime,
waitTime.isNotEmpty ? waitTime : '--',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1.0),
),
),
// Text(
// alarmLevel,
// style: TextStyle(
// fontSize: 24.sp,
// fontWeight: FontWeight.bold,
// color: const Color.fromRGBO(255, 77, 79, 1.0),
// ),
// ),
],
),
GestureDetector(
......
......@@ -46,22 +46,15 @@ class VoltageOperateBlock extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 64.w,
width: 66.w,
height: 54.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color.fromRGBO(235, 244, 255, 1.0),
borderRadius: BorderRadius.circular(12.r),
// border: Border.all(
// color: const Color.fromRGBO(66, 165, 245, 1.0),
// width: 2.w,
// style: BorderStyle.solid,
// ),
),
child: Icon(
Icons.thermostat_outlined,
color: const Color.fromRGBO(66, 165, 245, 1.0),
size: 40.sp,
),
child: Image.asset('lib/assets/icon/thermostat_bg.png',
width: 16.w, height: 32.h),
),
SizedBox(width: 16.w),
Expanded(
......@@ -103,7 +96,7 @@ class VoltageOperateBlock extends StatelessWidget {
width: double.infinity,
margin: EdgeInsets.only(left: 80.w, bottom: 20.h),
child: Text(
'电压${voltageValue}V (正常≤${normalVoltage}V)·$voltageWaitTime',
'电压${voltageValue} (正常≤${normalVoltage}V)·$voltageWaitTime',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1.0),
......
......@@ -12,9 +12,6 @@ class InspectionCubit extends Cubit<InspectionState> {
static const int pageSize = 20;
/// 完整房间列表缓存,用于在切换 Tab 时按 runStatus 重新过滤
List<InspectionRoomBO> _allRoomsRaw = const [];
InspectionCubit({
InspectionRoomService? roomService,
InspectionDeviceService? deviceService,
......@@ -30,21 +27,22 @@ class InspectionCubit extends Cubit<InspectionState> {
Future<void> initData() async {
try {
final rooms = await _roomService.getRooms();
_allRoomsRaw = rooms;
final tabCounts = _computeTabCounts(rooms);
final filteredRooms = _filterRooms(rooms, state.selectedTab);
final tabCounts = _computeTabCounts(rooms.runStatus);
final roomGroups = rooms.roomGroups;
// 仅当首次进入(之前没有选过房间)时,默认选中过滤后的第一个房间
final initialRoomIds = filteredRooms.isNotEmpty
? [filteredRooms.entries.first.key]
: <int>[];
// 默认选中第一个区域、第一个房间
final defaultAreaIndex = roomGroups.isNotEmpty ? 0 : 0;
final defaultRoomId = roomGroups.isNotEmpty &&
roomGroups[defaultAreaIndex].rooms.isNotEmpty
? roomGroups[defaultAreaIndex].rooms.first.roomId
: null;
emit(state.copyWith(
isLoading: false,
error: null,
allRooms: filteredRooms,
selectedRoomIds: initialRoomIds,
roomGroups: roomGroups,
selectedAreaIndex: defaultAreaIndex,
selectedRoomId: defaultRoomId,
tabCounts: tabCounts,
));
} catch (e) {
......@@ -55,20 +53,30 @@ class InspectionCubit extends Cubit<InspectionState> {
}
}
/// 从顶层 runStatus 汇总计算各 tab 数量
Map<DeviceStatusTab, int> _computeTabCounts(
InspectionRoomRunStatusBO runStatus) {
final total =
runStatus.normal + runStatus.warning + runStatus.offline + runStatus.fault;
return {
DeviceStatusTab.total: total,
DeviceStatusTab.on: runStatus.normal,
DeviceStatusTab.off: 0,
DeviceStatusTab.offline: runStatus.offline,
DeviceStatusTab.alarm: runStatus.warning,
DeviceStatusTab.fault: runStatus.fault,
};
}
/// 供 PagingController.fetchPage 回调使用
Future<List<InspectionDevice>> fetchPage(int pageKey) async {
final roomId = state.selectedRoomIds.length == 1
? state.selectedRoomIds.first
: null;
final result = await _deviceService.getList(
pageSize: pageSize,
pageNum: pageKey,
roomId: roomId,
roomId: state.selectedRoomId,
runStatus: state.selectedTab.runStatus,
);
// 首页时记录总数,供翻页判断是否到底
if (pageKey == 1) {
emit(state.copyWith(totalCount: result.total));
}
......@@ -78,87 +86,20 @@ class InspectionCubit extends Cubit<InspectionState> {
.toList();
}
Map<DeviceStatusTab, int> _computeTabCounts(List<InspectionRoomBO> rooms) {
int normal = 0;
int warning = 0;
int offline = 0;
int fault = 0;
for (final room in rooms) {
normal += room.runStatus.normal;
warning += room.runStatus.warning;
offline += room.runStatus.offline;
fault += room.runStatus.fault;
}
return {
DeviceStatusTab.total: normal + warning + offline + fault,
DeviceStatusTab.on: normal,
DeviceStatusTab.off: 0,
DeviceStatusTab.offline: offline,
DeviceStatusTab.alarm: warning,
DeviceStatusTab.fault: fault,
};
}
/// 根据 Tab 的 runStatus 过滤房间:
/// - total / runStatus 为 null → 返回全部
/// - 其他 → 仅保留对应状态计数 > 0 的房间
Map<int, String> _filterRooms(
List<InspectionRoomBO> rooms, DeviceStatusTab tab) {
final result = <int, String>{};
for (final room in rooms) {
if (_roomMatchesTab(room, tab)) {
result[room.roomId] = room.roomNumber;
}
}
return result;
}
bool _roomMatchesTab(InspectionRoomBO room, DeviceStatusTab tab) {
switch (tab) {
case DeviceStatusTab.total:
return true;
case DeviceStatusTab.on:
return room.runStatus.normal > 0;
case DeviceStatusTab.off:
case DeviceStatusTab.offline:
return room.runStatus.offline > 0;
case DeviceStatusTab.alarm:
return room.runStatus.warning > 0;
case DeviceStatusTab.fault:
return room.runStatus.fault > 0;
}
}
void selectTab(DeviceStatusTab tab) {
final filteredRooms = _filterRooms(_allRoomsRaw, tab);
// 过滤后剔除已不存在的已选房间;若全被剔除则默认选第一个
final keptSelection = state.selectedRoomIds
.where(filteredRooms.containsKey)
.toList();
final newSelectedRoomIds = keptSelection.isNotEmpty
? keptSelection
: (filteredRooms.isNotEmpty
? [filteredRooms.entries.first.key]
: <int>[]);
emit(state.copyWith(selectedTab: tab));
}
void selectArea(int index) {
if (index < 0 || index >= state.roomGroups.length) return;
final rooms = state.roomGroups[index].rooms;
emit(state.copyWith(
selectedTab: tab,
allRooms: filteredRooms,
selectedRoomIds: newSelectedRoomIds,
selectedAreaIndex: index,
selectedRoomId: rooms.isNotEmpty ? rooms.first.roomId : null,
));
}
void toggleRoom(int roomId) {
final List<int> newSelected = List.from(state.selectedRoomIds);
if (newSelected.contains(roomId)) {
newSelected.remove(roomId);
} else {
newSelected.clear();
newSelected.add(roomId);
}
emit(state.copyWith(selectedRoomIds: newSelected));
void selectRoom(int roomId) {
emit(state.copyWith(selectedRoomId: roomId));
}
}
\ No newline at end of file
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:smart_hotel_app/models/bo/inspection_device_bo.dart';
import 'package:smart_hotel_app/models/bo/inspection_room_bo.dart';
enum DeviceStatusTab { total, on, off, offline, alarm, fault }
......@@ -61,6 +62,8 @@ class InspectionDevice {
static IconData _mapIcon(String iconType) {
switch (iconType) {
case 'curtain':
return Icons.blinds_outlined;
case 'gateway':
return Icons.wifi;
case 'sensor':
......@@ -107,8 +110,9 @@ class InspectionState extends Equatable {
final bool isLoading;
final String? error;
final DeviceStatusTab selectedTab;
final List<int> selectedRoomIds;
final Map<int, String> allRooms;
final int selectedAreaIndex;
final int? selectedRoomId;
final List<InspectionRoomGroupsBO> roomGroups;
final Map<DeviceStatusTab, int> tabCounts;
final int totalCount;
......@@ -116,8 +120,9 @@ class InspectionState extends Equatable {
this.isLoading = false,
this.error,
this.selectedTab = DeviceStatusTab.total,
this.selectedRoomIds = const [],
this.allRooms = const {},
this.selectedAreaIndex = 0,
this.selectedRoomId,
this.roomGroups = const [],
this.tabCounts = const {
DeviceStatusTab.total: 0,
DeviceStatusTab.on: 0,
......@@ -133,8 +138,9 @@ class InspectionState extends Equatable {
bool? isLoading,
String? error,
DeviceStatusTab? selectedTab,
List<int>? selectedRoomIds,
Map<int, String>? allRooms,
int? selectedAreaIndex,
int? selectedRoomId,
List<InspectionRoomGroupsBO>? roomGroups,
Map<DeviceStatusTab, int>? tabCounts,
int? totalCount,
}) {
......@@ -142,8 +148,9 @@ class InspectionState extends Equatable {
isLoading: isLoading ?? this.isLoading,
error: error,
selectedTab: selectedTab ?? this.selectedTab,
selectedRoomIds: selectedRoomIds ?? this.selectedRoomIds,
allRooms: allRooms ?? this.allRooms,
selectedAreaIndex: selectedAreaIndex ?? this.selectedAreaIndex,
selectedRoomId: selectedRoomId ?? this.selectedRoomId,
roomGroups: roomGroups ?? this.roomGroups,
tabCounts: tabCounts ?? this.tabCounts,
totalCount: totalCount ?? this.totalCount,
);
......@@ -154,8 +161,9 @@ class InspectionState extends Equatable {
isLoading,
error,
selectedTab,
selectedRoomIds,
allRooms,
selectedAreaIndex,
selectedRoomId,
roomGroups,
tabCounts,
totalCount,
];
......
......@@ -33,7 +33,7 @@ class _InspectionDetailBodyState extends State<_InspectionDetailBody> {
late final PagingController<int, InspectionDevice> _pagingController;
late final InspectionCubit _cubit;
DeviceStatusTab? _lastTab;
List<int>? _lastRoomIds;
int? _lastRoomId;
@override
void initState() {
......@@ -55,9 +55,9 @@ class _InspectionDetailBodyState extends State<_InspectionDetailBody> {
void _checkFilterChanged(InspectionState state) {
if (_lastTab != state.selectedTab ||
_lastRoomIds != state.selectedRoomIds) {
_lastRoomId != state.selectedRoomId) {
_lastTab = state.selectedTab;
_lastRoomIds = state.selectedRoomIds;
_lastRoomId = state.selectedRoomId;
_pagingController.refresh();
}
}
......@@ -138,9 +138,11 @@ class _InspectionDetailBodyState extends State<_InspectionDetailBody> {
selectedTab: state.selectedTab,
tabCounts: state.tabCounts,
onTabSelected: cubit.selectTab,
selectedRoomIds: state.selectedRoomIds,
allRooms: state.allRooms,
onRoomToggle: cubit.toggleRoom,
selectedAreaIndex: state.selectedAreaIndex,
selectedRoomId: state.selectedRoomId,
roomGroups: state.roomGroups,
onAreaSelected: cubit.selectArea,
onRoomSelected: cubit.selectRoom,
),
),
),
......
......@@ -87,7 +87,7 @@ class DeviceListItem extends StatelessWidget {
),
SizedBox(width: 4.w),
Text(
device.time,
device.time.isNotEmpty ? device.time : '--',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1.0),
......
......@@ -68,6 +68,7 @@ class InspectionDeviceView extends StatelessWidget {
child: BlocBuilder<InspectionDeviceCubit, InspectionDeviceState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DeviceOverviewCard(
deviceName: state.deviceName,
......@@ -82,6 +83,15 @@ class InspectionDeviceView extends StatelessWidget {
),
if (state.inspectionHistory.isNotEmpty) ...[
SizedBox(height: 20.h),
Text(
'巡检历史 (${state.inspectionHistory.length})',
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(10, 13, 20, 1.0),
),
),
SizedBox(height: 10.h),
InspectionHistoryCard(
historyList: state.inspectionHistory,
),
......
......@@ -37,10 +37,11 @@ class DeviceOverviewCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 20.w,
children: [
Container(
width: 100.w,
height: 100.h,
width: 90.w,
height: 90.h,
decoration: const BoxDecoration(
color: Color.fromRGBO(235, 244, 255, 1.0),
shape: BoxShape.circle,
......@@ -48,13 +49,13 @@ class DeviceOverviewCard extends StatelessWidget {
child: Icon(
icon,
color: const Color.fromRGBO(66, 165, 245, 1.0),
size: 50.sp,
size: 40.sp,
),
),
SizedBox(width: 20.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5.h,
children: [
Text(
deviceName,
......@@ -64,7 +65,6 @@ class DeviceOverviewCard extends StatelessWidget {
color: const Color.fromRGBO(10, 13, 20, 1.0),
),
),
SizedBox(height: 12.h),
Text(
deviceType,
style: TextStyle(
......@@ -72,31 +72,30 @@ class DeviceOverviewCard extends StatelessWidget {
color: const Color.fromRGBO(100, 116, 139, 1.0),
),
),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 10.h),
decoration: BoxDecoration(
color: deviceStatus == '1'
? const Color.fromRGBO(225, 245, 238, 1.0)
: const Color.fromRGBO(255, 235, 238, 1.0),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
deviceStatus == '1' ? '在线' : '离线',
style: TextStyle(
fontSize: 28.sp,
color: deviceStatus == '1'
? const Color.fromRGBO(26, 188, 156, 1.0)
: const Color.fromRGBO(231, 76, 60, 1.0),
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
),
Container(
margin: EdgeInsets.only(left: 110.w, top: 5.h),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
decoration: BoxDecoration(
color: deviceStatus == '1'
? const Color.fromRGBO(225, 245, 238, 1.0)
: const Color.fromRGBO(255, 235, 238, 1.0),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
deviceStatus == '1' ? '在线' : '离线',
style: TextStyle(
fontSize: 24.sp,
color: deviceStatus == '1'
? const Color.fromRGBO(209, 250, 229, 1.0)
: const Color.fromRGBO(231, 76, 60, 1.0),
),
),
),
SizedBox(height: 32.h),
Text(
'基本信息',
......
......@@ -24,15 +24,6 @@ class InspectionHistoryCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'巡检历史 (${historyList.length})',
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(10, 13, 20, 1.0),
),
),
SizedBox(height: 20.h),
...historyList.map((item) => _buildHistoryItem(item)).toList(),
],
),
......@@ -77,7 +68,7 @@ class InspectionHistoryCard extends StatelessWidget {
spacing: 4.h,
children: [
Text(
item.inspectorName,
item.inspectorName.isNotEmpty ? item.inspectorName : '--',
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.bold,
......@@ -93,7 +84,7 @@ class InspectionHistoryCard extends StatelessWidget {
),
SizedBox(width: 4.w),
Text(
item.inspectionTime,
item.inspectionTime.isNotEmpty ? item.inspectionTime : '--',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1.0),
......@@ -106,21 +97,24 @@ class InspectionHistoryCard extends StatelessWidget {
],
),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 2.h),
decoration: BoxDecoration(
color: const Color.fromRGBO(225, 245, 238, 1.0),
borderRadius: BorderRadius.circular(40.r),
),
child: Row(
spacing: 4.w,
children: [
Icon(
Icons.check,
color: const Color.fromRGBO(26, 188, 156, 1.0),
size: 24.sp,
),
SizedBox(width: 4.w),
Text(
item.result,
item.result == '0' ? '巡检中' :
item.result == '1' ? '不通过' :
item.result == '2' ? '通过' :
item.result == '3' ? '警告' : '',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(26, 188, 156, 1.0),
......
......@@ -79,7 +79,8 @@ class InspectionItemsCard extends StatelessWidget {
),
SizedBox(width: 4.w),
Text(
item.result,
// item.result,
'通过',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(26, 188, 156, 1.0),
......
......@@ -49,8 +49,8 @@ class InspectionTopologyCubit extends Cubit<InspectionTopologyState> {
final rootNode = _buildTopologyTree(treeNodes);
emit(InspectionTopologyState(
deviceName: info.deviceName,
deviceModel: '${info.deviceCode}·${info.deviceLocation}',
deviceName: '${info.deviceLocation} ${info.deviceName}',
deviceModel: '${info.deviceCode}${info.deviceName}·${info.deviceLocation}',
topologyNodes: [rootNode],
deviceCount: _countAllNodes(rootNode),
connectionCount: _countConnections(rootNode),
......
......@@ -48,7 +48,7 @@ class DeviceDetailDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: EdgeInsets.only(top: 20.h),
margin: EdgeInsets.only(top: 40.h),
width: 120.w,
height: 8.h,
decoration: BoxDecoration(
......@@ -65,14 +65,14 @@ class DeviceDetailDialog extends StatelessWidget {
children: [
Container(
width: 80.w,
height: 80.h,
height: 64.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(239, 246, 255, 1),
borderRadius: BorderRadius.circular(20.r),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
_getDeviceIcon(node.name),
size: 80.sp,
size: 40.sp,
color: const Color.fromRGBO(59, 130, 246, 1),
),
),
......@@ -102,15 +102,15 @@ class DeviceDetailDialog extends StatelessWidget {
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 12.h,
horizontal: 16.w,
vertical: 2.h,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(209, 250, 229, 1),
borderRadius: BorderRadius.circular(40.r),
),
child: Text(
node.statusText?? '-',
node.statusText?? '--',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
......@@ -145,7 +145,7 @@ class DeviceDetailDialog extends StatelessWidget {
),
),
Text(
'${node.id}',
node.id,
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
......@@ -192,7 +192,7 @@ class DeviceDetailDialog extends StatelessWidget {
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromRGBO(59, 130, 246, 1),
backgroundColor: const Color.fromRGBO(73, 149, 234, 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40.r),
),
......
......@@ -26,7 +26,7 @@ class DeviceHeaderWidget extends StatelessWidget {
children: [
Container(
width: 64.w,
height: 64.h,
height: 53.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(219, 234, 254, 1),
borderRadius: BorderRadius.circular(20.r),
......
......@@ -56,6 +56,7 @@ class DeviceListCubit extends Cubit<DeviceListState> {
: (row.onlineStatus == '1' ? '正常' : '离线'),
color: row.statusTag.color,
lastUpdate: row.subtitle.text,
deviceTypeIcon: row.deviceTypeIcon,
);
}).toList();
}
......
......@@ -45,6 +45,7 @@ class DeviceInfo extends Equatable {
final String status;
final String lastUpdate;
final String color;
final String deviceTypeIcon;
const DeviceInfo({
this.deviceId = 0,
......@@ -55,11 +56,12 @@ class DeviceInfo extends Equatable {
this.status = '',
this.lastUpdate = '',
this.color = '',
this.deviceTypeIcon = '',
});
@override
List<Object?> get props => [deviceId, name, room, type, online,
status, lastUpdate, color];
status, lastUpdate, color, deviceTypeIcon];
}
class DeviceListState extends Equatable {
......
......@@ -10,29 +10,20 @@ class DeviceCard extends StatelessWidget {
required this.device,
});
IconData _getIconForType(DeviceTypeEnum type) {
switch (type) {
case DeviceTypeEnum.smartBreaker:
return Icons.power;
case DeviceTypeEnum.guestControl:
return Icons.settings_remote;
case DeviceTypeEnum.network:
IconData _getIconForType(String deviceTypeIcon) {
switch (deviceTypeIcon) {
case 'ac':
return Icons.air_outlined;
case 'light':
return Icons.light_outlined;
case 'tv':
return Icons.tv_outlined;
case 'curtain':
return Icons.blinds_outlined;
case 'wifi':
return Icons.wifi;
default:
return Icons.devices;
}
}
Color _getIconColor(DeviceTypeEnum type) {
switch (type) {
case DeviceTypeEnum.smartBreaker:
return const Color.fromRGBO(245, 158, 11, 1);
case DeviceTypeEnum.guestControl:
return const Color.fromRGBO(59, 130, 246, 1);
case DeviceTypeEnum.network:
return const Color.fromRGBO(16, 185, 129, 1);
default:
return const Color.fromRGBO(107, 114, 128, 1);
return Icons.devices_other;
}
}
......@@ -62,14 +53,14 @@ class DeviceCard extends StatelessWidget {
children: [
Container(
width: 64.w,
height: 58.h,
height: 53.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
color: _getIconColor(device.type).withOpacity(0.1),
color: const Color.fromRGBO(229, 241, 255, 1),
),
child: Icon(
_getIconForType(device.type),
color: _getIconColor(device.type),
_getIconForType(device.deviceTypeIcon),
color: const Color.fromRGBO(73, 149, 234, 1),
size: 36.sp,
),
),
......@@ -93,14 +84,14 @@ class DeviceCard extends StatelessWidget {
Text(
_getTypeLabel(device.type),
style: TextStyle(
fontSize: 20.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(107, 114, 128, 1),
),
),
Text(
'房间 ${device.room}',
style: TextStyle(
fontSize: 20.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(107, 114, 128, 1),
),
),
......@@ -122,38 +113,6 @@ class DeviceCard extends StatelessWidget {
fontWeight: FontWeight.w500,
),
),
// Column(
// crossAxisAlignment: CrossAxisAlignment.end,
// children: [
// Container(
// padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(12.r),
// color: device.online
// ? const Color.fromRGBO(220, 252, 231, 1)
// : const Color.fromRGBO(254, 226, 226, 1),
// ),
// child: Text(
// device.status,
// style: TextStyle(
// fontSize: 20.sp,
// color: device.online
// ? const Color.fromRGBO(21, 128, 61, 1)
// : const Color.fromRGBO(220, 38, 38, 1),
// fontWeight: FontWeight.bold,
// ),
// ),
// ),
// SizedBox(height: 8.h),
// Text(
// device.lastUpdate,
// style: TextStyle(
// fontSize: 18.sp,
// color: const Color.fromRGBO(156, 163, 175, 1),
// ),
// ),
// ],
// ),
],
),
);
......
......@@ -35,12 +35,13 @@ class DeviceFilterTab extends StatelessWidget {
color: Colors.white,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: tabs.map((tab) {
final isSelected = selectedType == tab['type'];
return GestureDetector(
onTap: () => onSelect(tab['type'] as DeviceTypeEnum),
child: Container(
margin: EdgeInsets.only(right: 16.w),
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
......
......@@ -66,23 +66,24 @@ class EnergyStatsCardBlock extends StatelessWidget {
Widget _buildCard(String value, String label, String? change, bool? isDown, Color valueColor) {
return Container(
padding: EdgeInsets.symmetric(vertical: 24.h),
height: 128.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4.h,
children: [
Text(
value,
style: TextStyle(
color: valueColor,
fontSize: 40.sp,
fontSize: 36.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
......@@ -90,7 +91,6 @@ class EnergyStatsCardBlock extends StatelessWidget {
fontSize: 24.sp,
),
),
SizedBox(height: 4.h),
if (change != null)
Row(
mainAxisAlignment: MainAxisAlignment.center,
......@@ -113,8 +113,6 @@ class EnergyStatsCardBlock extends StatelessWidget {
),
],
)
else
SizedBox(height: 24.h),
],
),
);
......
......@@ -25,20 +25,40 @@ class HourlyChartBlock extends StatelessWidget {
double get _chartInterval => _chartMaxY / 5;
double get _bottomInterval {
final len = hourlyData.length;
if (len <= 1) return 1;
// 最多显示约 5 个标签,按间距计算
return (len / 5).ceilToDouble();
Set<int> get _visibleLabelIndices {
final length = hourlyData.length;
if (length <= 10) {
return Set.from(List.generate(length, (i) => i));
}
final isOdd = length % 2 == 1;
if (isOdd) {
// 奇数:7 个标签,首尾都显示,中间 5 个均分 6 段
final step = (length - 1) / 6;
final indices = <int>{};
for (int i = 0; i <= 6; i++) {
indices.add((step * i).round());
}
return indices;
} else {
// 偶数:6 个标签,显示首和倒数第二个,中间 4 个均分 5 段
final step = (length - 2) / 5;
final indices = <int>{};
for (int i = 0; i <= 5; i++) {
indices.add((step * i).round());
}
return indices;
}
}
String _formatTimeLabel(int index) {
if (index < 0 || index >= timeLabels.length) return '';
final label = timeLabels[index];
return label;
// "00:00:00" / "00:00" → 只取整点小时,去掉前导 0
final parts = label.split(':');
final hour = parts.isNotEmpty ? parts[0] : label;
return '${int.tryParse(hour) ?? 0}';
// final parts = label.split(':');
// final hour = parts.isNotEmpty ? parts[0] : label;
// return '${int.tryParse(hour) ?? 0}';
}
@override
......@@ -81,10 +101,14 @@ class HourlyChartBlock extends StatelessWidget {
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: _bottomInterval,
interval: 1,
reservedSize: 30,
getTitlesWidget: (value, meta) {
final label = _formatTimeLabel(value.toInt());
final i = value.toInt();
if (!_visibleLabelIndices.contains(i)) {
return const SizedBox.shrink();
}
final label = _formatTimeLabel(i);
return Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
......
......@@ -45,6 +45,17 @@ class WeeklyPowerChart extends StatelessWidget {
return _timeLabels[index];
}
String _formatLeftTitle(double value) {
return '${value.toInt()}';
}
/// 根据最大数值的位数动态计算左侧保留宽度
double get _leftReservedSize {
final maxLabel = _maxY.toInt().toString();
// 每位数字约 10px(22.sp 字号)
return maxLabel.length * 8;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
......@@ -142,14 +153,16 @@ class WeeklyPowerChart extends StatelessWidget {
interval: _horizontalInterval,
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()}',
_formatLeftTitle(value),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 22.sp,
),
);
},
reservedSize: 50,
reservedSize: _leftReservedSize,
),
),
),
......
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/models/bo/room_bo.dart';
import 'package:smart_hotel_app/repositories/room_repository.dart';
import 'package:smart_hotel_app/services/room_service.dart';
import 'package:smart_hotel_app/views/report/room/cubit/room_report_state.dart';
......@@ -17,13 +18,26 @@ class RoomReportCubit extends Cubit<RoomReportState> {
try {
final overview = await _service.getOverview(floorId);
final activeIndex = overview.floorList.indexWhere((f) => f.isActive);
// 过滤掉每个房间 deviceList 中名称含"分控"的设备
final filteredDetailRooms = overview.roomDetailList.map((room) {
return RoomDetailListBO(
roomBasicInfo: room.roomBasicInfo,
statusTags: room.statusTags,
deviceList: room.deviceList
.where((d) => !d.deviceName.contains('分控'))
.toList(),
);
}).toList();
emit(state.copyWith(
isLoading: false,
summary: overview.summary,
floors: overview.floorList,
selectedFloorIndex: activeIndex >= 0 ? activeIndex : 0,
roomGrid: overview.roomGrid,
detailRooms: overview.roomDetailList,
detailRooms: filteredDetailRooms,
selectedRoomId: null,
));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
......@@ -34,7 +48,12 @@ class RoomReportCubit extends Cubit<RoomReportState> {
if (state.selectedFloorIndex == index) return;
final floorId = state.floors[index].floorId;
emit(state.copyWith(selectedFloorIndex: index));
emit(state.copyWith(selectedFloorIndex: index, selectedRoomId: null));
loadData(floorId: floorId);
}
void selectRoom(int roomId) {
final newId = state.selectedRoomId == roomId ? null : roomId;
emit(state.copyWith(selectedRoomId: newId));
}
}
......@@ -2,6 +2,8 @@ import 'package:equatable/equatable.dart';
import 'package:smart_hotel_app/models/bo/room_bo.dart';
class RoomReportState extends Equatable {
static const _nothing = Object();
final bool isLoading;
final String? error;
final RoomOverviewSummaryBO summary;
......@@ -9,6 +11,7 @@ class RoomReportState extends Equatable {
final int selectedFloorIndex;
final List<RoomGridBO> roomGrid;
final List<RoomDetailListBO> detailRooms;
final int? selectedRoomId;
const RoomReportState({
this.isLoading = false,
......@@ -18,6 +21,7 @@ class RoomReportState extends Equatable {
this.selectedFloorIndex = 0,
this.roomGrid = const [],
this.detailRooms = const [],
this.selectedRoomId,
});
RoomReportState copyWith({
......@@ -28,6 +32,7 @@ class RoomReportState extends Equatable {
int? selectedFloorIndex,
List<RoomGridBO>? roomGrid,
List<RoomDetailListBO>? detailRooms,
Object? selectedRoomId = _nothing,
bool clearError = false,
}) {
return RoomReportState(
......@@ -38,6 +43,7 @@ class RoomReportState extends Equatable {
selectedFloorIndex: selectedFloorIndex ?? this.selectedFloorIndex,
roomGrid: roomGrid ?? this.roomGrid,
detailRooms: detailRooms ?? this.detailRooms,
selectedRoomId: identical(selectedRoomId, _nothing) ? this.selectedRoomId : selectedRoomId as int?,
);
}
......@@ -50,5 +56,6 @@ class RoomReportState extends Equatable {
selectedFloorIndex,
roomGrid,
detailRooms,
selectedRoomId,
];
}
......@@ -59,52 +59,63 @@ class ReportRoomDetailView extends StatelessWidget {
}
return SingleChildScrollView(
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: Column(
children: [
SizedBox(height: 10.h),
if (state.floors.isNotEmpty)
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)),
),
child: FloorSelector(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: Column(
children: [
SizedBox(height: 10.h),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
),
child: Column(
children: [
if (state.floors.isNotEmpty)
FloorSelector(
floorNames: state.floors.map((f) => f.floorName).toList(),
selectedIndex: state.selectedFloorIndex,
),
),
if (state.floors.isNotEmpty && state.selectedFloorIndex < state.floors.length)
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24.r)),
if (state.floors.isNotEmpty && state.selectedFloorIndex < state.floors.length)
Padding(
padding: EdgeInsets.only(bottom: 28.h),
child: RoomGrid(
rooms: state.roomGrid,
selectedRoomId: state.selectedRoomId,
onRoomTap: (roomId) {
context.read<RoomReportCubit>().selectRoom(roomId);
},
),
),
padding: EdgeInsets.only(bottom: 28.w),
child: RoomGrid(rooms: state.roomGrid),
Divider(
height: 1.h,
thickness: 0.5,
color: const Color.fromRGBO(229, 231, 235, 1),
indent: 28.w,
endIndent: 28.w,
),
SizedBox(height: 10.h),
StatusStats(summary: state.summary),
SizedBox(height: 10.h),
if (state.isLoading)
const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator(color: Color.fromRGBO(66, 165, 245, 1.0))),
),
...state.detailRooms.map((room) {
return Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: RoomDetailCard(room: room),
);
}).toList(),
SizedBox(height: 10.h),
],
StatusStats(summary: state.summary),
if (state.isLoading)
const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator(color: Color.fromRGBO(66, 165, 245, 1.0))),
),
...state.detailRooms
.where((room) =>
state.selectedRoomId == null ||
room.roomBasicInfo.roomId == state.selectedRoomId)
.map((room) {
return Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: RoomDetailCard(room: room),
);
}),
],
),
),
),
],
SizedBox(height: 10.h),
],
),
),
);
}
......
......@@ -167,7 +167,9 @@ class _DeviceControl extends StatelessWidget {
case 'tv':
return Icons.tv_outlined;
case 'curtain':
return Icons.curtains_outlined;
return Icons.blinds_outlined;
case 'wifi':
return Icons.wifi;
default:
return Icons.devices_other;
}
......@@ -191,7 +193,9 @@ class _DeviceControl extends StatelessWidget {
SizedBox(width: 16.w),
Expanded(
child: Text(
device.deviceName,
device.deviceName.contains('-')
? device.deviceName.split('-').last
: device.deviceName,
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(59, 130, 246, 1),
......
......@@ -4,8 +4,15 @@ import 'package:smart_hotel_app/models/bo/room_bo.dart';
class RoomGrid extends StatelessWidget {
final List<RoomGridBO> rooms;
final int? selectedRoomId;
final Function(int) onRoomTap;
const RoomGrid({super.key, required this.rooms});
const RoomGrid({
super.key,
required this.rooms,
required this.selectedRoomId,
required this.onRoomTap,
});
@override
Widget build(BuildContext context) {
......@@ -18,7 +25,11 @@ class RoomGrid extends StatelessWidget {
crossAxisCount: 5,
mainAxisSpacing: 16.h,
crossAxisSpacing: 16.w,
children: rooms.map((room) => _RoomItem(room: room)).toList(),
children: rooms.map((room) => _RoomItem(
room: room,
isSelected: room.roomId == selectedRoomId,
onTap: () => onRoomTap(room.roomId),
)).toList(),
),
);
}
......@@ -26,8 +37,14 @@ class RoomGrid extends StatelessWidget {
class _RoomItem extends StatelessWidget {
final RoomGridBO room;
final bool isSelected;
final VoidCallback onTap;
const _RoomItem({required this.room});
const _RoomItem({
required this.room,
required this.isSelected,
required this.onTap,
});
Color get _bgColor {
switch (room.statusColor) {
......@@ -55,34 +72,19 @@ class _RoomItem extends StatelessWidget {
}
}
Color? get _borderColor {
switch (room.statusColor) {
case 'blue':
return const Color.fromRGBO(73, 149, 234, 1);
case 'yellow':
return const Color.fromRGBO(250, 204, 21, 1);
case 'default':
case 'gray':
default:
return null;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
},
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: _bgColor,
borderRadius: BorderRadius.circular(16.r),
// border: _borderColor != null
// ? Border.all(color: _borderColor!, width: 1.w)
// : null,
border: isSelected
? Border.all(color: const Color.fromRGBO(80, 162, 255, 1), width: 1.w)
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
......
......@@ -64,7 +64,7 @@ class ServiceIndexCubit extends Cubit<ServiceIndexState> {
floors: floors,
));
// 默认选中第一个楼层,加载其全部房间数据(不选区域类型)
// 默认选中第一个楼层,加载其全部房间数据,再默认选中第一个区域和房间
if (floors.isNotEmpty) {
final firstFloor = floors.first;
final firstFloorId = firstFloor.keys.first;
......@@ -72,6 +72,7 @@ class ServiceIndexCubit extends Cubit<ServiceIndexState> {
_updateAreasForFloor(firstFloorLabel);
selectFloor(firstFloorLabel);
await _loadAllRooms(floorId: firstFloorId);
_selectFirstTypeAndRoom();
}
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
......@@ -204,7 +205,7 @@ class ServiceIndexCubit extends Cubit<ServiceIndexState> {
}
/// UI 层切换楼层(切换时重置类型/房间/搜索,重新加载数据)
void toggleFloor(String floorName, int floorId) {
Future<void> toggleFloor(String floorName, int floorId) async {
final isSameFloor = state.selectedFloor == floorName;
final newFloor = isSameFloor ? null : floorName;
......@@ -227,17 +228,30 @@ class ServiceIndexCubit extends Cubit<ServiceIndexState> {
));
if (newFloor != null) {
_loadAllRooms(floorId: floorId);
await _loadAllRooms(floorId: floorId);
_selectFirstTypeAndRoom();
}
}
/// UI 层切换区域类型(点击切换,再点取消,重置房间)
/// UI 层切换区域类型(点击切换,再点取消;切换后默认选中该类型第一个房间)
void toggleType(String type) {
final newType = state.selectedType != type ? type : null;
String? newRoom;
int? newRoomId;
if (newType != null) {
final matchedRoom = state.rooms.firstWhere(
(r) => r['areaType'] == newType,
orElse: () => <String, dynamic>{},
);
if (matchedRoom.isNotEmpty) {
newRoom = matchedRoom['number'] as String;
newRoomId = matchedRoom['roomId'] as int;
}
}
emit(state.copyWith(
selectedType: newType,
selectedRoom: null,
selectedRoomId: null,
selectedRoom: newRoom,
selectedRoomId: newRoomId,
));
}
......@@ -327,4 +341,22 @@ class ServiceIndexCubit extends Cubit<ServiceIndexState> {
}
return null;
}
/// 默认选中第一个区域类型和该类型下的第一个房间
void _selectFirstTypeAndRoom() {
final areas = state.areas;
final rooms = state.rooms;
if (areas.isEmpty || rooms.isEmpty) return;
final defaultType = areas.first;
final defaultRoom = rooms.firstWhere(
(r) => r['areaType'] == defaultType,
orElse: () => rooms.first,
);
emit(state.copyWith(
selectedType: defaultType,
selectedRoom: defaultRoom['number'] as String,
selectedRoomId: defaultRoom['roomId'] as int,
));
}
}
\ No newline at end of file
......@@ -185,46 +185,6 @@ class RoomManagementBlock extends StatelessWidget {
);
}
Widget _buildRoomDetail(ServiceIndexState state, ServiceIndexCubit cubit) {
final room = cubit.getSelectedRoomDetail();
if (room == null) return const SizedBox.shrink();
final items = [
('房间有无人', room['occupancyStatus'] == '1' ? '有人' : '无人'),
('房间状态', room['occupancyStatus'] == '1' ? '入住' : '空闲'),
('清洁灯', room['cleanStatus'] == '2' ? '已完成' : '待清洁'),
('设备开关', room['devicePowerStatus'] == '1' ? '开' : '关'),
];
return Column(
children: items.map((item) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.$1,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
Text(
item.$2,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(73, 149, 234, 1),
fontWeight: FontWeight.w500,
),
),
],
),
);
}).toList(),
);
}
Widget _buildRoomGrid(ServiceIndexState state, ServiceIndexCubit cubit) {
final filteredRooms = cubit.getFilteredRooms();
const crossAxisCount = 12;
......@@ -397,6 +357,46 @@ class RoomManagementBlock extends StatelessWidget {
);
}
Widget _buildRoomDetail(ServiceIndexState state, ServiceIndexCubit cubit) {
final room = cubit.getSelectedRoomDetail();
if (room == null) return const SizedBox.shrink();
final items = [
('房间有无人', room['occupancyStatus'] == '1' ? '有人' : '无人'),
('房间状态', room['occupancyStatus'] == '1' ? '入住' : '空闲'),
// ('清洁灯', room['cleanStatus'] == '2' ? '已完成' : '待清洁'),
// ('设备开关', room['devicePowerStatus'] == '1' ? '开' : '关'),
];
return Column(
children: items.map((item) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.$1,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
Text(
item.$2,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(73, 149, 234, 1),
fontWeight: FontWeight.w500,
),
),
],
),
);
}).toList(),
);
}
Widget _buildBottomButtons(ServiceIndexState state, ServiceIndexCubit cubit, BuildContext context) {
final isRoomSelected = state.selectedRoom != null;
return Row(
......
......@@ -19,7 +19,7 @@ class ServiceStatsBlock extends StatelessWidget {
width: double.infinity,
padding: EdgeInsets.only(top: 20.w, bottom: 20.w),
child: Row(
spacing: 12.w,
spacing: 20.w,
children: [
_buildStatCard(
'待响应',
......@@ -44,13 +44,12 @@ class ServiceStatsBlock extends StatelessWidget {
Widget _buildStatCard(String label, String value, Color color) {
return Expanded(
child: Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
height: 112.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
),
child: Column(
spacing: 5.h,
children: [
Text(
value,
......
......@@ -38,20 +38,34 @@ class ServiceRoomCubit extends Cubit<ServiceRoomState> {
wifiOnlineStatus: basic.wifiOnlineStatus,
);
final roomDevices = detail.deviceStatusList.map((d) {
final roomDevices = detail.deviceStatusList
.where((d) => !d.deviceName.contains('分控'))
.map((d) {
return RoomDevice(
name: d.deviceName,
icon: _mapDeviceIcon(d.deviceTypeName, d.deviceTypeIcon),
name: d.deviceName.contains('-')
? d.deviceName.split('-').last
: d.deviceName,
icon: _mapDeviceIcon(d.deviceTypeIcon),
isOn: d.powerStatus == '1' || d.powerStatus == 'on',
statusText: _buildDeviceStatusText(d),
);
}).toList();
final controlDevices = detail.deviceControl.subDeviceControls.map((c) {
// 从 deviceStatusList 建立 deviceId → deviceTypeIcon 的映射
final iconMap = <int, String>{};
for (final d in detail.deviceStatusList) {
iconMap[d.deviceId] = d.deviceTypeIcon;
}
final controlDevices = detail.deviceControl.subDeviceControls
.where((c) => !c.deviceName.contains('分控'))
.map((c) {
return ControlDevice(
deviceId: c.deviceId,
name: c.deviceName,
icon: _mapDeviceIcon(c.deviceTypeName, ''),
name: c.deviceName.contains('-')
? c.deviceName.split('-').last
: c.deviceName,
icon: _mapDeviceIcon(iconMap[c.deviceId] ?? ''),
isOn: c.powerOn,
detailText: c.currentParamDisplay.isNotEmpty ? c.currentParamDisplay : null,
deviceTypeName: c.deviceTypeName,
......@@ -114,44 +128,30 @@ class ServiceRoomCubit extends Cubit<ServiceRoomState> {
}
}
IconData _mapDeviceIcon(String deviceTypeName, String deviceTypeIcon) {
// Try to match by deviceTypeName first
final name = deviceTypeName.toLowerCase();
if (name.contains('空调') || name.contains('air')) {
return Icons.air_outlined;
}
if (name.contains('照明') || name.contains('灯') || name.contains('light')) {
return Icons.light_outlined;
}
if (name.contains('电视') || name.contains('tv')) {
return Icons.tv_outlined;
}
if (name.contains('窗帘') || name.contains('curtain')) {
return Icons.curtains_closed_outlined;
}
// Fallback: match by deviceTypeIcon string
IconData _mapDeviceIcon(String deviceTypeIcon) {
final icon = deviceTypeIcon.toLowerCase();
if (icon.contains('air') || icon.contains('ac')) {
if (icon.contains('air-vent')) {
return Icons.air_outlined;
}
if (icon.contains('light')) {
if (icon.contains('lightbulb')) {
return Icons.light_outlined;
}
if (icon.contains('tv')) {
return Icons.tv_outlined;
}
if (icon.contains('curtain')) {
if (icon.contains('panel-top')) {
return Icons.curtains_closed_outlined;
}
if (icon.contains('wifi')) {
return Icons.wifi;
}
return Icons.devices_other;
}
/// 房间总电源开关
Future<void> toggleMainPowerForSwitch(bool value) async {
if (state.isPoweringOn || state.isPoweringOff) return;
final loadingKey = value ? 'isPoweringOn' : 'isPoweringOff';
// final loadingKey = value ? 'isPoweringOn' : 'isPoweringOff';
emit(state.copyWith(
isPoweringOn: value,
isPoweringOff: !value,
......
......@@ -87,18 +87,44 @@ class ServiceRoomDetailView extends StatelessWidget {
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20.h),
RoomOverviewCard(
roomNumber: state.roomNumber,
roomStatus: state.roomStatus,
statusTags: state.statusTags,
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
),
child: Column(
children: [
RoomOverviewCard(
roomNumber: state.roomNumber,
roomStatus: state.roomStatus,
statusTags: state.statusTags,
),
Divider(
height: 0.5.h,
color: const Color.fromRGBO(100, 116, 139, 0.2),
thickness: 0.5,
indent: 28.w,
endIndent: 28.w,
),
RoomDeviceStatusCard(
devices: state.roomDevices,
),
],
),
),
SizedBox(height: 20.h),
RoomDeviceStatusCard(
devices: state.roomDevices,
Text(
'设备控制',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(10, 13, 20, 1.0),
),
),
SizedBox(height: 20.h),
SizedBox(height: 10.h),
DeviceControlCard(
mainPowerOn: state.mainPowerOn,
controlDevices: state.controlDevices,
......
......@@ -30,17 +30,8 @@ class DeviceControlCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备控制',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(10, 13, 20, 1.0),
),
),
SizedBox(height: 20.h),
_buildMainPowerControl(),
SizedBox(height: 24.h),
SizedBox(height: 30.h),
Text(
'分设备控制',
style: TextStyle(
......@@ -49,14 +40,10 @@ class DeviceControlCard extends StatelessWidget {
color: const Color.fromRGBO(10, 13, 20, 1.0),
),
),
SizedBox(height: 30.h),
...controlDevices.asMap().entries.map((entry) {
final index = entry.key;
final device = entry.value;
return Padding(
padding: EdgeInsets.only(bottom: index < controlDevices.length - 1 ? 20.h : 0),
child: _buildDeviceControl(index, device),
);
return _buildDeviceControl(index, device);
}).toList(),
],
),
......@@ -134,24 +121,25 @@ class DeviceControlCard extends StatelessWidget {
Widget _buildDeviceControl(int index, ControlDevice device) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
SizedBox(
height: 80.h,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDeviceTap != null ? () => onDeviceTap!(index, device) : null,
child: Row(
spacing: 8.w,
children: [
Icon(
device.icon,
color: const Color.fromRGBO(66, 165, 245, 1.0),
size: 36.sp,
),
SizedBox(width: 8.w),
Text(
device.name,
style: TextStyle(
......@@ -164,23 +152,22 @@ class DeviceControlCard extends StatelessWidget {
),
),
),
),
FlutterSwitch(
width: 80.w,
height: 35.h,
valueFontSize: 20.sp,
toggleSize: 25.h,
borderRadius: 20.r,
padding: 4.w,
value: device.isOn,
activeColor: const Color.fromRGBO(80, 162, 255, 1),
inactiveColor: const Color.fromRGBO(210, 213, 218, 1),
onToggle: (value) => onDeviceToggle(index, value),
),
],
FlutterSwitch(
width: 80.w,
height: 35.h,
valueFontSize: 20.sp,
toggleSize: 25.h,
borderRadius: 20.r,
padding: 4.w,
value: device.isOn,
activeColor: const Color.fromRGBO(80, 162, 255, 1),
inactiveColor: const Color.fromRGBO(210, 213, 218, 1),
onToggle: (value) => onDeviceToggle(index, value),
),
],
),
),
if (index < controlDevices.length - 1) ...[
SizedBox(height: 30.h),
Divider(
height: 0.5.h,
color: const Color.fromRGBO(100, 116, 139, 0.2),
......
......@@ -61,13 +61,17 @@ class RoomDeviceStatusCard extends StatelessWidget {
color: const Color.fromRGBO(66, 165, 245, 1.0),
size: 36.sp,
),
Text(
device.name,
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(66, 165, 245, 1.0),
fontWeight: FontWeight.bold,
),
Expanded(
child: Text(
device.name,
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(66, 165, 245, 1.0),
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment