Commit 1244f87e authored by huqu's avatar huqu

fix bugs

parent 5a154d67
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="@string/app_name"
android:name="${applicationName}"
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<item android:drawable="@android:color/white" />
<item>
<bitmap
......
......@@ -52,11 +52,15 @@ class ElectricityTodayBO extends Equatable {
class AlertPendingBO extends Equatable {
final int count;
final int todayTotal;
const AlertPendingBO({required this.count});
const AlertPendingBO({required this.count, required this.todayTotal});
factory AlertPendingBO.fromJson(Map<String, dynamic> json) {
return AlertPendingBO(count: _parseInt(json['count']));
return AlertPendingBO(
count: _parseInt(json['count']),
todayTotal: _parseInt(json['todayTotal']),
);
}
@override
......
......@@ -136,13 +136,13 @@ class DeviceListTabCountBO extends Equatable {
final int all;
final int airSwitch;
final int guestControl;
final int network;
final int gateway;
const DeviceListTabCountBO({
required this.all,
required this.airSwitch,
required this.guestControl,
required this.network,
required this.gateway,
});
factory DeviceListTabCountBO.fromJson(Map<String, dynamic> json) {
......@@ -150,12 +150,12 @@ class DeviceListTabCountBO extends Equatable {
all: _parseInt(json['all']),
airSwitch: _parseInt(json['airSwitch']),
guestControl: _parseInt(json['guestControl']),
network: _parseInt(json['network']),
gateway: _parseInt(json['gateway']),
);
}
@override
List<Object?> get props => [all, airSwitch, guestControl, network];
List<Object?> get props => [all, airSwitch, guestControl, gateway];
}
class DeviceListBO extends Equatable {
......
......@@ -13,6 +13,7 @@ class InspectionDeviceBO extends Equatable {
final String onlineStatus;
final String powerStatus;
final int roomId;
final String deviceCategory;
const InspectionDeviceBO({
required this.deviceId,
......@@ -27,6 +28,7 @@ class InspectionDeviceBO extends Equatable {
required this.onlineStatus,
required this.powerStatus,
required this.roomId,
required this.deviceCategory,
});
factory InspectionDeviceBO.fromJson(Map<String, dynamic> json) {
......@@ -43,6 +45,7 @@ class InspectionDeviceBO extends Equatable {
onlineStatus: json['onlineStatus'] as String? ?? '',
powerStatus: json['powerStatus'] as String? ?? '',
roomId: int.tryParse(json['roomId']?.toString() ?? '') ?? 0,
deviceCategory: json['deviceCategory'] as String? ?? '',
);
}
......@@ -60,6 +63,7 @@ class InspectionDeviceBO extends Equatable {
onlineStatus,
powerStatus,
roomId,
deviceCategory,
];
}
......
......@@ -8,14 +8,14 @@ class DeviceListRepository {
Future<ResponseModel<DeviceListBO>> getList({
required int pageSize,
required int pageNum,
int? deviceTypeId,
String? deviceTypeId,
}) {
final params = <String, dynamic>{
'pageSize': pageSize,
'pageNum': pageNum,
};
if (deviceTypeId != null) {
params['deviceTypeId'] = deviceTypeId;
params['accessType'] = deviceTypeId;
}
return DioRequest.instance.get<DeviceListBO>(
......
......@@ -10,7 +10,7 @@ class DeviceListService {
Future<DeviceListBO> getList({
required int pageSize,
required int pageNum,
int? deviceTypeId,
String? deviceTypeId,
}) async {
final result = await _repository.getList(
pageSize: pageSize,
......
......@@ -12,22 +12,27 @@ class TemperatureCharBlock extends StatelessWidget {
double get _maxX => alarmInfo.temperatureSpots.length > 1
? (alarmInfo.temperatureSpots.length - 1).toDouble()
: 24;
: 1;
double get _minY => alarmInfo.yaxisMin;
double get _maxY => alarmInfo.yaxisMax > alarmInfo.yaxisMin
double get _maxY {
final raw = alarmInfo.yaxisMax > alarmInfo.yaxisMin
? alarmInfo.yaxisMax
: alarmInfo.yaxisMin + 100;
double get _horizontalInterval {
final interval = (_maxY - _minY) / 5;
return interval > 0 ? interval : 20;
// 留 10% 余量,防止顶部标签贴边
return raw * 1.1;
}
double get _bottomInterval => alarmInfo.temperatureSpots.length > 1
? (alarmInfo.temperatureSpots.length - 1) / 4
: 6;
double get _horizontalInterval => _maxY / 5;
double get _bottomInterval {
final len = alarmInfo.temperatureSpots.length;
if (len <= 1) return 1;
if (len <= 6) return 1;
if (len <= 12) return 2;
return (len - 1) / 4;
}
String _formatBottomTitle(double value) {
final index = value.toInt();
......@@ -46,7 +51,7 @@ class TemperatureCharBlock extends StatelessWidget {
return Container(
width: double.infinity,
margin: EdgeInsets.only(top: 10.h),
padding: EdgeInsets.all(25.w),
padding: EdgeInsets.symmetric(horizontal: 50.w, vertical: 25.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.r),
color: Colors.white,
......@@ -58,9 +63,7 @@ class TemperatureCharBlock extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
alarmInfo.chartTitle.isEmpty
? '24小时温度趋势'
: alarmInfo.chartTitle,
alarmInfo.chartTitle,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
......@@ -75,7 +78,7 @@ class TemperatureCharBlock extends StatelessWidget {
),
],
),
SizedBox(height: 20.h),
SizedBox(height: 30.h),
SizedBox(
height: 250.h,
child: LineChart(
......@@ -109,7 +112,7 @@ class TemperatureCharBlock extends StatelessWidget {
_formatBottomTitle(value),
style: TextStyle(
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 14.sp,
fontSize: 24.sp,
),
);
},
......@@ -125,7 +128,7 @@ class TemperatureCharBlock extends StatelessWidget {
'${value.toInt()}',
style: TextStyle(
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 14.sp,
fontSize: 24.sp,
),
);
},
......
......@@ -42,6 +42,9 @@ class DeviceCubit extends Cubit<DeviceState> {
final tempData = detail.trendChart.dataPoints
.map((p) => p.temperature.toInt())
.toList();
final timeLabels = detail.trendChart.dataPoints
.map((p) => p.time)
.toList();
return DeviceInfo(
deviceName: basic.deviceName,
......@@ -55,6 +58,7 @@ class DeviceCubit extends Cubit<DeviceState> {
temperature: params.deviceTemperature,
powerData: powerData,
tempData: tempData,
timeLabels: timeLabels,
);
}
......
......@@ -11,6 +11,7 @@ class DeviceInfo extends Equatable {
final String temperature;
final List<int> powerData;
final List<int> tempData;
final List<String> timeLabels;
const DeviceInfo({
required this.deviceName,
......@@ -23,6 +24,7 @@ class DeviceInfo extends Equatable {
required this.temperature,
required this.powerData,
required this.tempData,
required this.timeLabels,
});
@override
......@@ -37,6 +39,7 @@ class DeviceInfo extends Equatable {
temperature,
powerData,
tempData,
timeLabels,
];
}
......
......@@ -32,11 +32,11 @@ class DeviceParmsBlock extends StatelessWidget {
Row(
children: [
Expanded(
child: _buildParamCard(deviceInfo.voltage, '电压'),
child: _buildParamCard(deviceInfo.voltage, '电压', 'V'),
),
SizedBox(width: 15.w),
Expanded(
child: _buildParamCard(deviceInfo.current, '电流'),
child: _buildParamCard(deviceInfo.current, '电流', 'A'),
),
],
),
......@@ -44,11 +44,11 @@ class DeviceParmsBlock extends StatelessWidget {
Row(
children: [
Expanded(
child: _buildParamCard(deviceInfo.power, '有功功率', isGreen: true),
child: _buildParamCard(deviceInfo.power, '有公功率', 'W', isGreen: true),
),
SizedBox(width: 15.w),
Expanded(
child: _buildParamCard(deviceInfo.temperature, '设备温度'),
child: _buildParamCard(deviceInfo.temperature, '设备温度', '°C'),
),
],
),
......@@ -57,30 +57,56 @@ class DeviceParmsBlock extends StatelessWidget {
);
}
Widget _buildParamCard(String value, String label, {bool isGreen = false}) {
String _formatValue(String raw) {
final d = double.tryParse(raw);
if (d == null) return raw;
// 去掉末尾多余的 0,整数不显示小数点
final formatted = d.toStringAsFixed(2);
final parts = formatted.split('.');
if (parts.length == 2 && parts[1] == '00') return parts[0];
return formatted.replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r'\.$'), '');
}
Widget _buildParamCard(String value, String label, String unit, {bool isGreen = false}) {
return Container(
padding: EdgeInsets.all(25.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.r),
color: Color.fromRGBO(229, 241, 255, 1),
color: const Color.fromRGBO(229, 241, 255, 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
Text.rich(
TextSpan(
children: [
TextSpan(
text: _formatValue(value),
style: TextStyle(
fontSize: 40.sp,
color: isGreen ? const Color.fromRGBO(20, 184, 166, 1)
: const Color.fromRGBO(73, 149, 234, 1),
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: unit,
style: TextStyle(
fontSize: 40.sp,
color: isGreen ? Color.fromRGBO(20, 184, 166, 1) : Color.fromRGBO(73, 149, 234, 1),
color: isGreen ? const Color.fromRGBO(20, 184, 166, 1)
: const Color.fromRGBO(73, 149, 234, 1),
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(height: 8.h),
Text(
label,
style: TextStyle(
fontSize: 24.sp,
color: Color.fromRGBO(100, 116, 139, 1),
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
],
......
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fl_chart/fl_chart.dart';
......@@ -35,7 +36,7 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
'24小时功率趋势',
style: TextStyle(
fontSize: 24.sp,
color: Color.fromRGBO(100, 116, 139, 1),
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
Container(
......@@ -43,7 +44,7 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
borderRadius: BorderRadius.circular(12.r),
color: Colors.transparent,
border: Border.all(
color: Color.fromRGBO(73, 149, 234, 0.5),
color: const Color.fromRGBO(73, 149, 234, 0.5),
width: 1.5.w,
),
),
......@@ -57,12 +58,12 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
],
),
SizedBox(height: 20.h),
Container(
SizedBox(
height: 250.h,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceBetween,
maxY: 100,
maxY: _calcMaxY(_selectedIndex),
minY: 0,
groupsSpace: 10,
barTouchData: BarTouchData(enabled: false),
......@@ -70,9 +71,9 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: 20,
horizontalInterval: _calcInterval(_selectedIndex),
getDrawingHorizontalLine: (value) {
return FlLine(
return const FlLine(
color: Color.fromRGBO(209, 213, 219, 1),
strokeWidth: 1.5,
dashArray: [5, 5],
......@@ -81,18 +82,18 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
),
titlesData: FlTitlesData(
show: true,
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 20,
interval: _calcInterval(_selectedIndex),
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()}',
style: TextStyle(
color: Color.fromRGBO(156, 163, 175, 1),
fontSize: 14.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 24.sp,
),
);
},
......@@ -102,13 +103,19 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 3,
interval: _calcBottomInterval(),
getTitlesWidget: (value, meta) {
return Text(
'11:04',
final i = value.toInt();
final labels = widget.deviceInfo.timeLabels;
final label = i >= 0 && i < labels.length ? labels[i] : '';
return Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
label,
style: TextStyle(
color: Color.fromRGBO(156, 163, 175, 1),
fontSize: 14.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 20.sp,
),
),
);
},
......@@ -138,13 +145,14 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
color: isSelected ? Color.fromRGBO(219, 234, 254, 1) : Colors.transparent,
color: isSelected ? const Color.fromRGBO(219, 234, 254, 1) : Colors.transparent,
),
child: Text(
title,
style: TextStyle(
fontSize: 24.sp,
color: isSelected ? Color.fromRGBO(73, 149, 234, 1) : Color.fromRGBO(100, 116, 139, 1),
color: isSelected ? const Color.fromRGBO(73, 149, 234, 1) :
const Color.fromRGBO(100, 116, 139, 1),
),
),
),
......@@ -154,8 +162,8 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
List<BarChartGroupData> _buildBarGroups(int index) {
final data = index == 0 ? widget.deviceInfo.powerData : widget.deviceInfo.tempData;
final Color mainColor = index == 0
? Color.fromRGBO(59, 130, 246, 1)
: Color.fromRGBO(251, 191, 36, 1);
? const Color.fromRGBO(59, 130, 246, 1)
: const Color.fromRGBO(251, 191, 36, 1);
return List.generate(
data.length,
......@@ -175,4 +183,32 @@ class _PowerCharBlockState extends State<PowerCharBlock> {
},
).where((group) => group.barRods.isNotEmpty).toList();
}
double _calcMaxY(int index) {
final data = index == 0 ? widget.deviceInfo.powerData : widget.deviceInfo.tempData;
if (data.isEmpty) return 100;
final max = data.reduce((a, b) => a > b ? a : b).toDouble();
if (max == 0) return 100;
// 向上取整到最近的 nice number,并留 20% 余量
final raw = max * 1.2;
final magnitude = pow(10, (log(raw) / ln10).floor()).toDouble();
final normalized = raw / magnitude;
final nice = normalized <= 1 ? 1
: normalized <= 2 ? 2
: normalized <= 5 ? 5
: 10;
return nice * magnitude;
}
double _calcInterval(int index) {
return _calcMaxY(index) / 5;
}
double _calcBottomInterval() {
final length = widget.deviceInfo.timeLabels.length;
if (length <= 6) return 1;
if (length <= 12) return 2;
if (length <= 24) return 3;
return (length / 6).ceilToDouble();
}
}
\ No newline at end of file
......@@ -49,7 +49,7 @@ class InspectionDevice {
factory InspectionDevice.fromInspectionDeviceBO(InspectionDeviceBO bo) {
return InspectionDevice(
id: bo.deviceId.toString(),
icon: _mapIcon(bo.deviceTypeIcon),
icon: _mapIcon(bo.deviceCategory),
name: bo.deviceName,
type: bo.deviceTypeName,
location: bo.roomName,
......@@ -67,7 +67,7 @@ class InspectionDevice {
return Icons.thermostat;
case 'camera':
return Icons.videocam;
case 'light':
case 'lamp':
return Icons.light;
case 'air_conditioner':
case 'ac':
......
......@@ -145,7 +145,24 @@ class _InspectionDetailBodyState extends State<_InspectionDetailBody> {
),
),
SliverToBoxAdapter(
child: SizedBox(height: 20.h),
child: Column(
children: [
Row(
children: [
SizedBox(width: 28.w),
Text(
'设备列表(${pagingState.items != null ? pagingState.items!.length : 0})',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(10, 13, 20, 1.0),
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 16.h),
],
),
),
DeviceList(
pagingState: pagingState,
......
......@@ -26,21 +26,21 @@ class DeviceListItem extends StatelessWidget {
child: Row(
children: [
Container(
width: 64.w,
height: 66.h,
width: 80.w,
height: 64.h,
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: 1.w,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(8.r),
// border: Border.all(
// color: const Color.fromRGBO(66, 165, 245, 1.0),
// width: 1.w,
// style: BorderStyle.solid,
// ),
),
child: Icon(
device.icon,
color: const Color.fromRGBO(66, 165, 245, 1.0),
size: 32.sp,
size: 42.sp,
),
),
SizedBox(width: 16.w),
......@@ -58,7 +58,7 @@ class DeviceListItem extends StatelessWidget {
color: const Color.fromRGBO(10, 13, 20, 1.0),
),
),
SizedBox(width: 8.w),
SizedBox(width: 18.w),
Text(
device.status,
style: TextStyle(
......@@ -83,13 +83,13 @@ class DeviceListItem extends StatelessWidget {
Icon(
Icons.access_time,
color: const Color.fromRGBO(100, 116, 139, 1.0),
size: 20.sp,
size: 28.sp,
),
SizedBox(width: 4.w),
Text(
device.time,
style: TextStyle(
fontSize: 20.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1.0),
),
),
......@@ -98,18 +98,10 @@ class DeviceListItem extends StatelessWidget {
],
),
),
Container(
width: 48.w,
height: 48.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(242, 243, 245, 1.0),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icon(
Icons.chevron_right,
color: const Color.fromRGBO(100, 116, 139, 1.0),
size: 24.sp,
),
size: 56.sp,
),
],
),
......
......@@ -142,16 +142,65 @@ class FilterPanel extends StatelessWidget {
),
)
else
Wrap(
spacing: 5.w,
runSpacing: 12.h,
children: roomEntries.map((entry) {
// Wrap(
// spacing: 12.w,
// runSpacing: 12.h,
// children: roomEntries.map((entry) {
// final isSelected = selectedRoomIds.contains(entry.key);
// return GestureDetector(
// onTap: () => onRoomToggle(entry.key),
// child: Container(
// width: 120.w,
// height: 60.h,
// alignment: Alignment.center,
// padding: EdgeInsets.symmetric(horizontal: 5.w),
// decoration: BoxDecoration(
// color: isSelected
// ? const Color.fromRGBO(235, 244, 255, 1.0)
// : Colors.white,
// borderRadius: BorderRadius.circular(12.r),
// border: Border.all(
// color: isSelected
// ? const Color.fromRGBO(66, 165, 245, 1.0)
// : const Color.fromRGBO(200, 208, 220, 1.0),
// width: 1.w,
// ),
// ),
// child: Text(
// entry.value,
// style: TextStyle(
// fontSize: 24.sp,
// color: isSelected
// ? const Color.fromRGBO(66, 165, 245, 1.0)
// : const Color.fromRGBO(100, 116, 139, 1.0),
// fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
// ),
// textAlign: TextAlign.center,
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// ),
// ),
// );
// }).toList(),
// ),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 50 / 30,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
),
itemCount: roomEntries.length,
itemBuilder: (context, index) {
final entry = roomEntries[index];
final isSelected = selectedRoomIds.contains(entry.key);
return GestureDetector(
onTap: () => onRoomToggle(entry.key),
child: Container(
width: 120.w,
padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 12.h),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 5.w),
decoration: BoxDecoration(
color: isSelected
? const Color.fromRGBO(235, 244, 255, 1.0)
......@@ -173,10 +222,13 @@ class FilterPanel extends StatelessWidget {
: const Color.fromRGBO(100, 116, 139, 1.0),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
);
}).toList(),
},
),
],
);
......
......@@ -22,8 +22,8 @@ class InspectionDeviceCubit extends Cubit<InspectionDeviceState> {
area: '',
responsiblePerson: '',
lastInspectionTime: '',
inspectionHistory: [],
inspectionItems: [],
inspectionHistory: const [],
inspectionItems: const [],
icon: Icons.devices,
)) {
loadDeviceData();
......
......@@ -54,9 +54,9 @@ class InspectionDeviceView extends StatelessWidget {
fontWeight: FontWeight.w500,
),
),
Icon(
const Icon(
Icons.arrow_forward,
color: const Color.fromRGBO(100, 116, 139, 1),
color: Color.fromRGBO(100, 116, 139, 1),
),
],
),
......
......@@ -41,8 +41,8 @@ class DeviceOverviewCard extends StatelessWidget {
Container(
width: 100.w,
height: 100.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(235, 244, 255, 1.0),
decoration: const BoxDecoration(
color: Color.fromRGBO(235, 244, 255, 1.0),
shape: BoxShape.circle,
),
child: Icon(
......
......@@ -87,16 +87,26 @@ class InspectionTopologyCubit extends Cubit<InspectionTopologyState> {
}
// 递归构建节点并计算布局位置
// depth < 3:水平排列;depth ≥ 3:垂直向下延申
TopologyNode buildNode(TreeNodeBO node, int depth, double xCenter, double xSpan) {
final children = childrenMap[node.deviceId] ?? [];
final childNodes = <TopologyNode>[];
const verticalFromDepth = 3;
if (children.isNotEmpty) {
if (depth >= verticalFromDepth) {
// 垂直排列:每个子节点深度递增,挂在父节点正下方
for (int i = 0; i < children.length; i++) {
childNodes.add(buildNode(children[i], depth + 1 + i, xCenter, xSpan));
}
} else {
// 水平排列:子节点同深度,x 分散
for (int i = 0; i < children.length; i++) {
final childX = children.length == 1
? xCenter
: xCenter - xSpan / 2 + (xSpan * i / (children.length - 1));
childNodes.add(buildNode(children[i], depth + 1, childX, xSpan * 0.55));
childNodes.add(buildNode(children[i], depth + 1, childX, xSpan));
}
}
}
......@@ -107,19 +117,21 @@ class InspectionTopologyCubit extends Cubit<InspectionTopologyState> {
statusText: node.statusText,
gatewayId: node.gatewayId?.toString(),
x: xCenter.clamp(0.05, 0.95),
y: (0.08 + depth * 0.17).clamp(0.02, 0.98),
y: depth < 3
? (0.05 + depth * 0.25).clamp(0.02, 0.98)
: (0.78 + (depth - 3) * 0.10).clamp(0.02, 0.98),
children: childNodes,
);
}
if (roots.length == 1) {
return buildNode(roots.first, 0, 0.5, 0.7);
return buildNode(roots.first, 0, 0.5, 0.95);
} else {
// 多个根节点:创建一个虚拟根,将所有根均匀分布在第二层
final virtualChildren = <TopologyNode>[];
for (int i = 0; i < roots.length; i++) {
final xPos = 0.1 + (0.8 * i / (roots.length - 1));
virtualChildren.add(buildNode(roots[i], 1, xPos, 0.35));
virtualChildren.add(buildNode(roots[i], 1, xPos, 0.5));
}
return TopologyNode(
id: '0',
......
......@@ -98,13 +98,19 @@ class InspectionTopologyPage extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 24.h),
child: Column(
children: [
SizedBox(height: 10.h),
StatusLegendWidget(),
const StatusLegendWidget(),
SizedBox(height: 20.h),
DeviceHeaderWidget(
deviceName: state.deviceName,
deviceModel: state.deviceModel,
),
Divider(
height: 0.5.h,
color: const Color.fromRGBO(100, 116, 139, 0.2),
thickness: 0.5,
indent: 28.w,
endIndent: 28.w,
),
NetworkTopologyWidget(
topologyNodes: state.topologyNodes,
deviceCount: state.deviceCount,
......
......@@ -16,8 +16,30 @@ class NetworkTopologyWidget extends StatelessWidget {
required this.onNodeTap,
});
/// 找到最大深度,并返回每个深度的节点总数
Map<int, int> _countByDepth(TopologyNode node, int depth) {
final map = <int, int>{};
_collectDepthCounts(node, depth, map);
return map;
}
void _collectDepthCounts(TopologyNode node, int depth, Map<int, int> map) {
map[depth] = (map[depth] ?? 0) + 1;
for (var child in node.children) {
_collectDepthCounts(child, depth + 1, map);
}
}
@override
Widget build(BuildContext context) {
// 计算最大深度 & 每个深度有多少节点
final depthCounts =
topologyNodes.isNotEmpty ? _countByDepth(topologyNodes[0], 0) : <int, int>{};
final maxDepth = depthCounts.keys.isEmpty ? 0 : depthCounts.keys.reduce((a, b) => a > b ? a : b);
// 最后一层节点数 > 3 时,叶子节点用竖向容器
final lastLevelCount = depthCounts[maxDepth] ?? 0;
final useVerticalLeaf = lastLevelCount > 3;
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 32.h),
decoration: BoxDecoration(
......@@ -77,6 +99,9 @@ class NetworkTopologyWidget extends StatelessWidget {
clickableNodes,
constraints.maxWidth,
constraints.maxHeight,
0,
maxDepth,
useVerticalLeaf,
);
return Stack(children: clickableNodes);
},
......@@ -95,13 +120,75 @@ class NetworkTopologyWidget extends StatelessWidget {
List<Widget> nodes,
double maxWidth,
double maxHeight,
int depth,
int maxDepth,
bool useVerticalLeaf,
) {
final centerX = maxWidth * node.x;
final centerY = maxHeight * node.y;
final nodeWidth = 150.w;
// final nodeHeight = node.name.length > 4 ? 160.h : 100.h;
final nodeHeight = 80.h;
// 跳过虚拟根节点
final isVirtualRoot = node.id == '0' && node.name == '根节点';
// depth ≥ 3 全部竖向;否则只有最后一层且叶子多时竖向
final isVerticalNode = depth >= 3 ||
(depth == maxDepth && node.children.isEmpty && useVerticalLeaf);
if (!isVirtualRoot) {
if (isVerticalNode) {
// 竖向椭圆容器
final vw = 48.w;
final vh = 144.h;
nodes.add(
Positioned(
left: centerX - vw / 2,
top: centerY - vh / 2,
width: vw,
height: vh,
child: GestureDetector(
onTap: () => onNodeTap(node),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color.fromRGBO(229, 241, 255, 1),
border: Border.all(
color: const Color.fromRGBO(80, 162, 255, 1),
width: 1.w,
),
borderRadius: BorderRadius.circular(vw / 2),
),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: () {
const maxChars = 4;
final chars = node.name.split('');
final display = chars.length > maxChars
? [...chars.take(maxChars - 1), '…']
: chars;
return display
.map((char) => Text(
char,
style: TextStyle(
color: const Color.fromRGBO(100, 116, 139, 1),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
))
.toList();
}(),
),
),
),
),
),
);
} else {
// 普通水平椭圆容器
final nodeWidth = 140.w;
final nodeHeight = 54.h;
nodes.add(
Positioned(
left: centerX - nodeWidth / 2,
......@@ -111,32 +198,35 @@ class NetworkTopologyWidget extends StatelessWidget {
child: GestureDetector(
onTap: () => onNodeTap(node),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color.fromRGBO(239, 246, 255, 1),
color: const Color.fromRGBO(229, 241, 255, 1),
border: Border.all(
color: const Color.fromRGBO(59, 130, 246, 1),
color: const Color.fromRGBO(80, 162, 255, 1),
width: 1.w,
),
borderRadius: BorderRadius.circular(60.r),
borderRadius: BorderRadius.circular(40.r),
),
child: Center(
child: Text(
node.name,
style: TextStyle(
color: const Color.fromRGBO(71, 85, 105, 1),
color: const Color.fromRGBO(100, 116, 139, 1),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
),
);
}
}
for (var child in node.children) {
_collectNodes(child, nodes, maxWidth, maxHeight);
_collectNodes(child, nodes, maxWidth, maxHeight, depth + 1, maxDepth, useVerticalLeaf);
}
}
}
......@@ -159,6 +249,14 @@ class TopologyPainter extends CustomPainter {
}
void _drawConnections(Canvas canvas, Size size, TopologyNode node, Paint paint) {
final isVirtualRoot = node.id == '0' && node.name == '根节点';
if (isVirtualRoot) {
for (var child in node.children) {
_drawConnections(canvas, size, child, paint);
}
return;
}
final centerX = size.width * node.x;
final centerY = size.height * node.y;
......
......@@ -26,7 +26,7 @@ class StatusLegendWidget extends StatelessWidget {
),
SizedBox(height: 24.h),
Wrap(
spacing: 32.w,
spacing: 24.w,
runSpacing: 20.h,
children: [
_buildLegendItem(const Color.fromRGBO(59, 130, 246, 1), '开启'),
......@@ -47,14 +47,14 @@ class StatusLegendWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32.w,
height: 32.h,
width: 24.w,
height: 24.h,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
SizedBox(width: 12.w),
SizedBox(width: 8.w),
Text(
label,
style: TextStyle(
......
......@@ -39,7 +39,7 @@ class DeviceListCubit extends Cubit<DeviceListState> {
tabAll: result.tabCount.all,
tabAirSwitch: result.tabCount.airSwitch,
tabGuestControl: result.tabCount.guestControl,
tabNetwork: result.tabCount.network,
tabNetwork: result.tabCount.gateway,
totalCount: result.total,
));
}
......@@ -54,6 +54,7 @@ class DeviceListCubit extends Cubit<DeviceListState> {
status: row.statusTag.text.isNotEmpty
? row.statusTag.text
: (row.onlineStatus == '1' ? '正常' : '离线'),
color: row.statusTag.color,
lastUpdate: row.subtitle.text,
);
}).toList();
......
......@@ -7,17 +7,17 @@ enum DeviceTypeEnum {
network,
}
/// deviceTypeId 映射: all→null, smartBreaker→1, guestControl→2, network→3
int? deviceTypeIdFromEnum(DeviceTypeEnum type) {
/// deviceTypeId 映射: all→null, smartBreaker→airSwitch, guestControl→guestControl, network→gateway
String? deviceTypeIdFromEnum(DeviceTypeEnum type) {
switch (type) {
case DeviceTypeEnum.all:
return null;
case DeviceTypeEnum.smartBreaker:
return 1;
return 'airSwitch';
case DeviceTypeEnum.guestControl:
return 2;
return 'guestControl';
case DeviceTypeEnum.network:
return 3;
return 'gateway';
}
}
......@@ -44,6 +44,7 @@ class DeviceInfo extends Equatable {
final bool online;
final String status;
final String lastUpdate;
final String color;
const DeviceInfo({
this.deviceId = 0,
......@@ -53,10 +54,12 @@ class DeviceInfo extends Equatable {
this.online = false,
this.status = '',
this.lastUpdate = '',
this.color = '',
});
@override
List<Object?> get props => [deviceId, name, room, type, online, status, lastUpdate];
List<Object?> get props => [deviceId, name, room, type, online,
status, lastUpdate, color];
}
class DeviceListState extends Equatable {
......
......@@ -97,10 +97,11 @@ class _ReportDeviceListBodyState extends State<_ReportDeviceListBody> {
SizedBox(height: 10.h),
DeviceFilterTab(
selectedType: state.selectedType,
tabAirSwitch: state.tabAirSwitch,
tabGuestControl: state.tabGuestControl,
tabNetwork: state.tabNetwork,
onSelect: (type) {
context
.read<DeviceListCubit>()
.selectType(type);
context.read<DeviceListCubit>().selectType(type);
},
),
SizedBox(height: 10.h),
......@@ -113,8 +114,7 @@ class _ReportDeviceListBodyState extends State<_ReportDeviceListBody> {
sliver: PagedSliverList<int, DeviceInfo>(
state: pagingState,
fetchNextPage: fetchNextPage,
builderDelegate:
PagedChildBuilderDelegate<DeviceInfo>(
builderDelegate: PagedChildBuilderDelegate<DeviceInfo>(
firstPageErrorIndicatorBuilder: (_) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
......
......@@ -61,10 +61,10 @@ class DeviceCard extends StatelessWidget {
child: Row(
children: [
Container(
width: 80.w,
height: 80.h,
width: 64.w,
height: 58.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
borderRadius: BorderRadius.circular(8.r),
color: _getIconColor(device.type).withOpacity(0.1),
),
child: Icon(
......@@ -77,6 +77,7 @@ class DeviceCard extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.h,
children: [
Text(
device.name,
......@@ -86,8 +87,8 @@ class DeviceCard extends StatelessWidget {
color: const Color.fromRGBO(31, 41, 55, 1),
),
),
SizedBox(height: 8.h),
Row(
spacing: 16.w,
children: [
Text(
_getTypeLabel(device.type),
......@@ -96,7 +97,6 @@ class DeviceCard extends StatelessWidget {
color: const Color.fromRGBO(107, 114, 128, 1),
),
),
SizedBox(width: 16.w),
Text(
'房间 ${device.room}',
style: TextStyle(
......@@ -109,38 +109,51 @@ class DeviceCard extends StatelessWidget {
],
),
),
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,
device.status,
style: TextStyle(
fontSize: 18.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
),
],
),
fontSize: 24.sp,
color:
device.color == 'danger' ? const Color.fromRGBO(220, 38, 38, 1) :
device.color == 'success' ? const Color.fromRGBO(20, 184, 166, 1) :
device.color == 'warning' ? const Color.fromRGBO(250, 204, 21, 1) :
device.color == 'info' ? const Color.fromRGBO(100, 116, 139, 1) :
const Color.fromRGBO(20, 184, 166, 1),
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),
// ),
// ),
// ],
// ),
],
),
);
......
......@@ -5,20 +5,26 @@ import 'package:smart_hotel_app/views/report/device/cubit/device_list_state.dart
class DeviceFilterTab extends StatelessWidget {
final DeviceTypeEnum selectedType;
final Function(DeviceTypeEnum) onSelect;
final int tabAirSwitch;
final int tabGuestControl;
final int tabNetwork;
const DeviceFilterTab({
super.key,
required this.selectedType,
required this.onSelect,
required this.tabAirSwitch,
required this.tabGuestControl,
required this.tabNetwork,
});
@override
Widget build(BuildContext context) {
final tabs = [
{'type': DeviceTypeEnum.all, 'label': '全部'},
{'type': DeviceTypeEnum.smartBreaker, 'label': '空开'},
{'type': DeviceTypeEnum.guestControl, 'label': '客控'},
{'type': DeviceTypeEnum.network, 'label': '网络'},
{'type': DeviceTypeEnum.smartBreaker, 'label': '空开($tabAirSwitch)'},
{'type': DeviceTypeEnum.guestControl, 'label': '客控($tabGuestControl)'},
{'type': DeviceTypeEnum.network, 'label': '网络($tabNetwork)'},
];
return Container(
......@@ -35,18 +41,23 @@ class DeviceFilterTab extends StatelessWidget {
return GestureDetector(
onTap: () => onSelect(tab['type'] as DeviceTypeEnum),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
borderRadius: BorderRadius.circular(8.r),
color: isSelected
? const Color.fromRGBO(59, 130, 246, 1)
? const Color.fromRGBO(73, 149, 234, 0.2)
: const Color.fromRGBO(243, 244, 246, 1),
border: isSelected ? Border.all(
width: 1.w,
color: const Color.fromRGBO(73, 149, 234, 0.5),
) : null,
),
child: Text(
tab['label'] as String,
tab['label'] as String ,
style: TextStyle(
fontSize: 24.sp,
color: isSelected ? Colors.white : const Color.fromRGBO(107, 114, 128, 1),
color: isSelected ? const Color.fromRGBO(73, 149, 234, 1) :
const Color.fromRGBO(107, 114, 128, 1),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
......
......@@ -45,9 +45,12 @@ class EnergyCubit extends Cubit<EnergyState> {
100;
}
// 将逐小时数据点提取为 List<double>
// 将逐小时数据点提取为 List<double> 和时间标签
final hourlyData =
data.hourlyUsageToday.hourlyPoints.map((p) => p.value).toList();
final hourlyTimeLabels = data.hourlyUsageToday.hourlyPoints
.map((p) => p.time.length >= 5 ? p.time.substring(0, 5) : p.time)
.toList();
// 将分区数据映射为 ZoneData 列表
final totalToday = data.zoneUsageRatio.totalToday;
......@@ -69,6 +72,7 @@ class EnergyCubit extends Cubit<EnergyState> {
monthBill: data.monthCostEstimate,
hourlyMaxValue: data.hourlyUsageToday.maxValue,
hourlyData: hourlyData,
hourlyTimeLabels: hourlyTimeLabels,
zoneData: zoneData,
);
}
......
......@@ -25,6 +25,7 @@ class EnergyState extends Equatable {
final double monthBill;
final double hourlyMaxValue;
final List<double> hourlyData;
final List<String> hourlyTimeLabels;
final List<ZoneData> zoneData;
const EnergyState({
......@@ -37,6 +38,7 @@ class EnergyState extends Equatable {
this.monthBill = 6982,
this.hourlyMaxValue = 0,
this.hourlyData = const [],
this.hourlyTimeLabels = const [],
this.zoneData = const [
ZoneData(name: '客房区域', percentage: 68, energy: 873),
ZoneData(name: '公共区域', percentage: 18, energy: 231),
......@@ -55,6 +57,7 @@ class EnergyState extends Equatable {
double? monthBill,
double? hourlyMaxValue,
List<double>? hourlyData,
List<String>? hourlyTimeLabels,
List<ZoneData>? zoneData,
bool clearError = false,
}) {
......@@ -68,6 +71,7 @@ class EnergyState extends Equatable {
monthBill: monthBill ?? this.monthBill,
hourlyMaxValue: hourlyMaxValue ?? this.hourlyMaxValue,
hourlyData: hourlyData ?? this.hourlyData,
hourlyTimeLabels: hourlyTimeLabels ?? this.hourlyTimeLabels,
zoneData: zoneData ?? this.zoneData,
);
}
......@@ -83,6 +87,7 @@ class EnergyState extends Equatable {
monthBill,
hourlyMaxValue,
hourlyData,
hourlyTimeLabels,
zoneData,
];
}
......@@ -78,6 +78,7 @@ class ReportEnergyDetailView extends StatelessWidget {
HourlyChartBlock(
hourlyData: state.hourlyData,
maxValue: state.hourlyMaxValue,
timeLabels: state.hourlyTimeLabels,
),
SizedBox(height: 16.h),
ZoneChartBlock(zoneData: state.zoneData),
......
......@@ -17,6 +17,13 @@ class EnergyStatsCardBlock extends StatelessWidget {
required this.monthBill,
});
String _fmt(double d) {
if (d == d.truncateToDouble()) return d.truncate().toString();
return d.toStringAsFixed(2)
.replaceAll(RegExp(r'0+$'), '')
.replaceAll(RegExp(r'\.$'), '');
}
@override
Widget build(BuildContext context) {
return Padding(
......@@ -25,9 +32,9 @@ class EnergyStatsCardBlock extends StatelessWidget {
children: [
Expanded(
child: _buildCard(
'${todayEnergy.toInt()}',
_fmt(todayEnergy),
'今日(kwh)',
'${todayEnergyChange.toInt()}%',
'${_fmt(todayEnergyChange)}%',
true,
const Color.fromRGBO(59, 130, 246, 1),
),
......@@ -35,9 +42,9 @@ class EnergyStatsCardBlock extends StatelessWidget {
SizedBox(width: 12.w),
Expanded(
child: _buildCard(
'${monthEnergy.toInt()}',
_fmt(monthEnergy),
'本月(kWh)',
'${monthEnergyChange.toInt()}%',
'${_fmt(monthEnergyChange)}%',
true,
const Color.fromRGBO(59, 130, 246, 1),
),
......@@ -45,7 +52,7 @@ class EnergyStatsCardBlock extends StatelessWidget {
SizedBox(width: 12.w),
Expanded(
child: _buildCard(
${monthBill.toInt()}',
${_fmt(monthBill)}',
'本月电费预估',
null,
null,
......@@ -59,7 +66,7 @@ class EnergyStatsCardBlock extends StatelessWidget {
Widget _buildCard(String value, String label, String? change, bool? isDown, Color valueColor) {
return Container(
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
padding: EdgeInsets.symmetric(vertical: 24.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.r),
......@@ -73,6 +80,7 @@ class EnergyStatsCardBlock extends StatelessWidget {
fontSize: 40.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
......
......@@ -5,27 +5,42 @@ import 'package:fl_chart/fl_chart.dart';
class HourlyChartBlock extends StatelessWidget {
final List<double> hourlyData;
final double maxValue;
final List<String> timeLabels;
const HourlyChartBlock({
super.key,
required this.hourlyData,
required this.maxValue,
required this.timeLabels,
});
double get _chartMaxY {
final dataMaxValue = maxValue > 0 ? maxValue : 0.0;
final listMaxValue =
final listMax =
hourlyData.isEmpty ? 0.0 : hourlyData.reduce((a, b) => a > b ? a : b);
final chartMaxValue =
dataMaxValue > listMaxValue ? dataMaxValue : listMaxValue;
if (chartMaxValue <= 0) {
return 100;
}
return chartMaxValue * 1.2;
final raw = maxValue > listMax ? maxValue : listMax;
if (raw <= 0) return 100;
// 留 10% 余量
return raw * 1.1;
}
double get _chartInterval => _chartMaxY / 5;
double get _bottomInterval {
final len = hourlyData.length;
if (len <= 1) return 1;
// 最多显示约 5 个标签,按间距计算
return (len / 5).ceilToDouble();
}
String _formatTimeLabel(int index) {
if (index < 0 || index >= timeLabels.length) return '';
final label = timeLabels[index];
// "00:00:00" / "00:00" → 只取整点小时,去掉前导 0
final parts = label.split(':');
final hour = parts.isNotEmpty ? parts[0] : label;
return '${int.tryParse(hour) ?? 0}';
}
@override
Widget build(BuildContext context) {
return Container(
......@@ -41,18 +56,19 @@ class HourlyChartBlock extends StatelessWidget {
Text(
'今日逐时用电',
style: TextStyle(
color: Colors.black87,
color: const Color.fromRGBO(100, 116, 139, 1),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 100.h),
SizedBox(height: 20.h),
SizedBox(
height: 200.h,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
alignment: BarChartAlignment.spaceBetween,
maxY: _chartMaxY,
minY: 0,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
......@@ -65,15 +81,37 @@ class HourlyChartBlock extends StatelessWidget {
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: _bottomInterval,
reservedSize: 30,
getTitlesWidget: _getBottomTitleWidget,
getTitlesWidget: (value, meta) {
final label = _formatTimeLabel(value.toInt());
return Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
label,
style: TextStyle(
fontSize: 20.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: _getLeftTitleWidget,
interval: _chartInterval,
reservedSize: 55,
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()}',
style: TextStyle(
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 20.sp,
),
);
},
),
),
),
......@@ -84,8 +122,8 @@ class HourlyChartBlock extends StatelessWidget {
horizontalInterval: _chartInterval,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: Color.fromRGBO(200, 200, 200, 1),
strokeWidth: 1,
color: Color.fromRGBO(209, 213, 219, 1),
strokeWidth: 1.5,
dashArray: [5, 5],
);
},
......@@ -99,48 +137,11 @@ class HourlyChartBlock extends StatelessWidget {
);
}
Widget _getBottomTitleWidget(double value, TitleMeta meta) {
const style = TextStyle(
color: Colors.black54,
fontSize: 14,
);
String text;
final index = value.toInt();
if (index % 4 == 0) {
if (index == 0) {
text = '00:00';
} else if (index == 24) {
text = '24:00';
} else {
text = '${index.toString().padLeft(2, '0')}:00';
}
} else {
text = '';
}
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
Widget _getLeftTitleWidget(double value, TitleMeta meta) {
const style = TextStyle(
color: Colors.black54,
fontSize: 14,
);
final text = value % _chartInterval == 0
? value.toStringAsFixed(value.truncateToDouble() == value ? 0 : 1)
: '';
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
List<BarChartGroupData> _buildBarGroups() {
return hourlyData.asMap().entries.map((entry) {
final index = entry.key;
final value = entry.value;
if (value == 0) return BarChartGroupData(x: index);
return BarChartGroupData(
x: index,
barRods: [
......
......@@ -22,7 +22,7 @@ class ZoneChartBlock extends StatelessWidget {
Text(
'分区用电占比',
style: TextStyle(
color: Colors.black87,
color: const Color.fromRGBO(100, 116, 139, 1),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
......@@ -49,7 +49,7 @@ class ZoneChartBlock extends StatelessWidget {
color: index == 0
? const Color.fromRGBO(59, 130, 246, 1)
: const Color.fromRGBO(107, 114, 128, 1),
borderRadius: BorderRadius.circular(3.r),
shape: BoxShape.circle,
),
),
SizedBox(width: 8.w),
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:smart_hotel_app/models/bo/dashboard_bo.dart';
import 'package:smart_hotel_app/repositories/dashboard_repository.dart';
import 'package:smart_hotel_app/services/dashboard_service.dart';
......@@ -29,10 +30,9 @@ class ReportIndexCubit extends Cubit<ReportIndexState> {
}
ReportIndexState _mapOverviewToState(DashboardOverviewBO overview) {
// 今日用电指标
final elec = overview.electricityToday;
final yesterdayVal = elec.yesterdayValue;
final todayVal = elec.value;
// 今日用电
final yesterdayVal = overview.electricityToday.yesterdayValue;
final todayVal = overview.electricityToday.value;
double changePercent = 0;
if (yesterdayVal > 0) {
changePercent = ((todayVal - yesterdayVal) / yesterdayVal * 100);
......@@ -40,6 +40,10 @@ class ReportIndexCubit extends Cubit<ReportIndexState> {
final isDown = changePercent <= 0;
final changeAbs = changePercent.abs().round();
//待处理告警
final count = overview.alertPending.count;
final todayTotal = overview.alertPending.todayTotal;
return state.copyWith(
isLoading: false,
reportHeader: {
......@@ -48,25 +52,30 @@ class ReportIndexCubit extends Cubit<ReportIndexState> {
},
reportMetrics: {
'powerUsage': {
'value': todayVal.toInt(),
'value': todayVal.toString(),
'unit': 'kWh',
'change': isDown ? -changeAbs : changeAbs,
'changeText': isDown ? '↓ $changeAbs%' : '↑ $changeAbs%',
'progress': (todayVal / 2000).clamp(0.0, 1.0),
'progress': (todayVal / yesterdayVal).clamp(0.0, 1.0),
},
'pendingAlerts': {
'value': overview.alertPending.count,
'value': count.toString(),
'unit': '条',
'change': null,
'changeText': overview.alertPending.count > 0 ? '需关注' : '无告警',
'progress': (overview.alertPending.count / 10).clamp(0.0, 1.0),
'changeText': count > 0 ? '需关注' : '无告警',
'progress': (count / todayTotal).clamp(0.0, 1.0),
},
},
weeklyPowerUsage: {
'title': '本周用电趋势',
'unit': 'kWh',
'dateRange': overview.electricityTrend.dateRange,
'data': _mapWeekDays(overview.electricityTrend.points),
'maxValue': overview.electricityTrend.maxValue,
'spots': _buildSpots(overview.electricityTrend.points),
'timeLabels': overview.electricityTrend.points.map((p) {
// "06-10 19:21:47" → "06-10"
return p.time.split(' ').first;
}).toList(),
},
roomStatusDistribution: {
'title': '客房状态分布',
......@@ -76,7 +85,7 @@ class ReportIndexCubit extends Cubit<ReportIndexState> {
deviceOnlineRate: {
'title': '设备在线率',
'rate': 1.0,
'rateText': '${overview.deviceTypeDistribution.totalDeviceCount}',
'rateText': '${overview.deviceTypeDistribution.totalDeviceCount}',
'devices': _mapDeviceTypes(overview.deviceTypeDistribution.items),
},
quickActions: {
......@@ -89,26 +98,19 @@ class ReportIndexCubit extends Cubit<ReportIndexState> {
);
}
/// 将趋势点映射为周几标签
List<Map<String, dynamic>> _mapWeekDays(List<DashboardTrendPointBO> points) {
const dayLabels = ['一', '二', '三', '四', '五', '六', '日'];
List<FlSpot> _buildSpots(List<DashboardTrendPointBO> points) {
return points.asMap().entries.map((entry) {
final index = entry.key;
final point = entry.value;
return {
'day': index < dayLabels.length ? dayLabels[index] : '${index + 1}',
'value': point.value.toInt(),
};
return FlSpot(entry.key.toDouble(), entry.value.value);
}).toList();
}
/// 映射客房状态并分配颜色
List<Map<String, dynamic>> _mapRoomStatuses(List<RoomStatusItemBO> items) {
const statusColors = [
Color.fromRGBO(59, 130, 246, 1),
Color.fromRGBO(148, 163, 184, 1),
Color.fromRGBO(96, 165, 250, 1),
Color.fromRGBO(249, 115, 22, 1),
Color.fromRGBO(73, 149, 234, 1),
Color.fromRGBO(100, 116, 139, 1),
Color.fromRGBO(139, 193, 255, 1),
Color.fromRGBO(250, 204, 21, 1),
];
return items.asMap().entries.map((entry) {
final index = entry.key;
......
......@@ -46,15 +46,15 @@ class DeviceOnlineRate extends StatelessWidget {
Row(
children: [
Text(
'共 $totalDevices 台',
'共$totalDevices台',
style: TextStyle(
fontSize: 22.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
),
SizedBox(width: 8.w),
Icon(
Icons.chevron_right,
Icons.arrow_forward_ios,
size: 24.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
......@@ -76,9 +76,9 @@ class DeviceOnlineRate extends StatelessWidget {
sections: [
PieChartSectionData(
value: rate,
color: const Color.fromRGBO(34, 197, 94, 1),
color: const Color.fromRGBO(49, 193, 177, 1),
title: '',
radius: 30.w,
radius: 20.w,
),
PieChartSectionData(
// value: 1 - rate,
......@@ -97,7 +97,7 @@ class DeviceOnlineRate extends StatelessWidget {
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(31, 41, 55, 1),
color: const Color.fromRGBO(20, 184, 166, 1),
),
),
],
......
......@@ -49,7 +49,7 @@ class QuickActionsBlock 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(16.r),
......
......@@ -15,7 +15,7 @@ class ReportHeaderBlock extends StatelessWidget {
reportHeader['title'] as String,
style: TextStyle(
fontSize: 36.sp,
fontWeight: FontWeight.normal,
fontWeight: FontWeight.w500,
color: const Color.fromRGBO(71, 71, 71, 1),
),
),
......
......@@ -19,9 +19,10 @@ class ReportMetricsBlock extends StatelessWidget {
value: powerUsage['value'],
unit: powerUsage['unit'] as String,
changeText: powerUsage['changeText'] as String,
changeColor: const Color.fromRGBO(16, 185, 129, 1),
changeColor: const Color.fromRGBO(20, 184, 166, 1),
valueColor: const Color.fromRGBO(73, 149, 234, 1),
progress: powerUsage['progress'] as double,
progressColor: const Color.fromRGBO(59, 130, 246, 1),
progressColor: const Color.fromRGBO(73, 149, 234, 1),
),
),
SizedBox(width: 20.w),
......@@ -31,9 +32,10 @@ class ReportMetricsBlock extends StatelessWidget {
value: pendingAlerts['value'],
unit: pendingAlerts['unit'] as String,
changeText: pendingAlerts['changeText'] as String,
changeColor: const Color.fromRGBO(251, 191, 36, 1),
changeColor: const Color.fromRGBO(250, 204, 21, 1),
valueColor: const Color.fromRGBO(250, 204, 21, 1),
progress: pendingAlerts['progress'] as double,
progressColor: const Color.fromRGBO(251, 191, 36, 1),
progressColor: const Color.fromRGBO(250, 204, 21, 1),
),
),
],
......@@ -42,10 +44,11 @@ class ReportMetricsBlock extends StatelessWidget {
Widget _buildMetricCard({
required String title,
required int value,
required String value,
required String unit,
required String changeText,
required Color changeColor,
required Color valueColor,
required double progress,
required Color progressColor,
}) {
......@@ -58,6 +61,9 @@ class ReportMetricsBlock extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
......@@ -65,16 +71,25 @@ class ReportMetricsBlock extends StatelessWidget {
color: const Color.fromRGBO(107, 114, 128, 1),
),
),
Text(
changeText,
style: TextStyle(
fontSize: 24.sp,
color: changeColor,
),
),
],
),
SizedBox(height: 12.h),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'$value',
value,
style: TextStyle(
fontSize: 40.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(31, 41, 55, 1),
color: valueColor,
),
),
SizedBox(width: 4.w),
......@@ -90,18 +105,6 @@ class ReportMetricsBlock extends StatelessWidget {
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Text(
changeText,
style: TextStyle(
fontSize: 20.sp,
color: changeColor,
),
),
],
),
SizedBox(height: 12.h),
ClipRRect(
borderRadius: BorderRadius.circular(4.r),
......
......@@ -43,16 +43,16 @@ class RoomStatusDistribution extends StatelessWidget {
Row(
children: [
Text(
'共 $totalRooms 间',
'共$totalRooms间',
style: TextStyle(
fontSize: 22.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
),
SizedBox(width: 8.w),
Icon(
Icons.arrow_forward_ios,
size: 20.sp,
size: 24.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
],
......@@ -65,6 +65,7 @@ class RoomStatusDistribution extends StatelessWidget {
final color = status['color'] as Color;
final name = status['name'] as String;
final percentage = totalRooms > 0 ? count / totalRooms : 0.0;
final percentText = '${(percentage * 100).toInt()}%';
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
......@@ -95,10 +96,10 @@ class RoomStatusDistribution extends StatelessWidget {
],
),
Text(
'$count',
'$count间($percentText)',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(55, 65, 81, 1),
color: color,
),
),
],
......
......@@ -9,17 +9,51 @@ class WeeklyPowerChart extends StatelessWidget {
const WeeklyPowerChart({super.key, required this.weeklyPowerUsage});
List<FlSpot> get _spots =>
(weeklyPowerUsage['spots'] as List<dynamic>?)?.cast<FlSpot>() ?? [];
List<String> get _timeLabels =>
(weeklyPowerUsage['timeLabels'] as List<dynamic>?)?.cast<String>() ?? [];
double get _minX => 0;
double get _maxX => _spots.length > 1
? (_spots.length - 1).toDouble()
: 1;
double get _minY => 0;
double get _maxY {
final raw = (weeklyPowerUsage['maxValue'] as num?)?.toDouble() ?? 100;
// 留 10% 余量,防止顶部标签贴边
return raw * 1.1;
}
double get _horizontalInterval => _maxY / 5;
double get _bottomInterval {
final len = _spots.length;
if (len <= 1) return 1;
if (len <= 6) return 1;
if (len <= 12) return 2;
return (len - 1) / 4;
}
String _formatBottomTitle(double value) {
final index = value.toInt();
if (index < 0 || index >= _timeLabels.length) return '';
return _timeLabels[index];
}
@override
Widget build(BuildContext context) {
final data = weeklyPowerUsage['data'] as List<Map<String, dynamic>>;
return GestureDetector(
onTap: () {
context.pushRoute(const ReportEnergyDetailRoute());
},
child: Container(
width: double.infinity,
padding: EdgeInsets.all(28.w),
padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 28.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.r),
color: Colors.white,
......@@ -41,16 +75,16 @@ class WeeklyPowerChart extends StatelessWidget {
Row(
children: [
Text(
weeklyPowerUsage['dateRange'] as String,
weeklyPowerUsage['dateRange'] as String? ?? '',
style: TextStyle(
fontSize: 22.sp,
fontSize: 24.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
),
SizedBox(width: 8.w),
Icon(
Icons.arrow_forward_ios,
size: 20.sp,
size: 24.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
],
......@@ -58,15 +92,25 @@ class WeeklyPowerChart extends StatelessWidget {
],
),
SizedBox(height: 20.h),
SizedBox(
Container(
height: 200.h,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceBetween,
maxY: 300,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
padding: EdgeInsets.only(right: 28.w),
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: _horizontalInterval,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: Color.fromRGBO(209, 213, 219, 1),
strokeWidth: 2,
dashArray: [5, 5],
);
},
),
titlesData: FlTitlesData(
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
......@@ -76,49 +120,65 @@ class WeeklyPowerChart extends StatelessWidget {
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: _bottomInterval,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < data.length) {
return Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
data[index]['day'] as String,
_formatBottomTitle(value),
style: TextStyle(
fontSize: 22.sp,
color: const Color.fromRGBO(156, 163, 175, 1),
),
),
);
}
return const SizedBox.shrink();
},
reservedSize: 28,
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: _horizontalInterval,
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()}',
style: TextStyle(
color: const Color.fromRGBO(156, 163, 175, 1),
fontSize: 22.sp,
),
);
},
reservedSize: 50,
),
),
),
borderData: FlBorderData(show: false),
gridData: const FlGridData(show: false),
barGroups: data.asMap().entries.map((entry) {
final index = entry.key;
final value = (entry.value['value'] as int).toDouble();
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: value,
minX: _minX,
maxX: _maxX,
minY: _minY,
maxY: _maxY,
lineBarsData: [
LineChartBarData(
spots: _spots,
isCurved: true,
color: const Color.fromRGBO(59, 130, 246, 1),
width: 24.w,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
gradient: const LinearGradient(
colors: [
Color.fromRGBO(191, 219, 254, 1),
Color.fromRGBO(219, 234, 254, 0.1),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
);
}).toList(),
),
),
),
......
......@@ -17,16 +17,13 @@ class FloorSelector extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
width: double.infinity,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 16.h),
padding: EdgeInsets.symmetric(vertical: 20.h),
child: Row(
children: floorNames.asMap().entries.map((entry) {
final index = entry.key;
final name = entry.value;
final isSelected = index == selectedIndex;
return Padding(
padding: EdgeInsets.only(right: 32.w),
return Expanded(
child: GestureDetector(
onTap: () {
context.read<RoomReportCubit>().selectFloor(index);
......@@ -38,18 +35,18 @@ class FloorSelector extends StatelessWidget {
style: TextStyle(
fontSize: 28.sp,
color: isSelected
? const Color.fromRGBO(59, 130, 246, 1)
: const Color.fromRGBO(156, 163, 175, 1),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
? const Color.fromRGBO(80, 162, 255, 1)
: const Color.fromRGBO(10, 13, 20, 1),
fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
),
),
SizedBox(height: 4.h),
Container(
width: 40.w,
width: 50.w,
height: 3.h,
decoration: BoxDecoration(
color: isSelected
? const Color.fromRGBO(59, 130, 246, 1)
? const Color.fromRGBO(80, 162, 255, 1)
: Colors.transparent,
borderRadius: BorderRadius.circular(2.r),
),
......@@ -60,7 +57,7 @@ class FloorSelector extends StatelessWidget {
);
}).toList(),
),
),
);
}
}
......@@ -20,10 +20,11 @@ class RoomDetailCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 16.w,
children: [
Container(
width: 60.w,
height: 60.h,
width: 64.w,
height: 53.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(219, 234, 254, 1),
borderRadius: BorderRadius.circular(12.r),
......@@ -34,9 +35,9 @@ class RoomDetailCard extends StatelessWidget {
size: 32.sp,
),
),
SizedBox(width: 16.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12.h,
children: [
Row(
children: [
......@@ -58,23 +59,23 @@ class RoomDetailCard extends StatelessWidget {
),
],
),
SizedBox(height: 12.h),
Row(
spacing: 12.w,
children: [
if (room.statusTags.isOccupied)
_StatusTag(
label: '有人',
isActive: room.statusTags.isOccupied,
),
SizedBox(width: 12.w),
if (room.statusTags.powerSupplyStatus)
_StatusTag(
label: '通电中',
isActive: room.statusTags.powerSupplyStatus,
),
SizedBox(width: 12.w),
_StatusTag(
label: 'wifi正常',
isActive: room.statusTags.wifiNormalStatus,
),
// _StatusTag(
// label: 'wifi正常',
// isActive: room.statusTags.wifiNormalStatus,
// ),
],
),
],
......@@ -84,13 +85,23 @@ class RoomDetailCard extends StatelessWidget {
SizedBox(height: 24.h),
Divider(
height: 1.h,
thickness: 0.5,
color: const Color.fromRGBO(229, 231, 235, 1),
),
SizedBox(height: 24.h),
// GridView.count(
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// crossAxisCount: 2,
// mainAxisSpacing: 16.h,
// crossAxisSpacing: 16.w,
// children: room.deviceList.map((device) => _DeviceControl(device: device)).toList(),
// ),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 3,
mainAxisSpacing: 16.h,
crossAxisSpacing: 16.w,
children: room.deviceList.map((device) => _DeviceControl(device: device)).toList(),
......@@ -113,21 +124,20 @@ class _StatusTag extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: isActive
? const Color.fromRGBO(167, 243, 208, 1)
? const Color.fromRGBO(20, 184, 166, 0.2)
: const Color.fromRGBO(229, 231, 235, 1),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4.w,
children: [
if (isActive)
Icon(
Icons.check,
size: 20.sp,
size: 24.sp,
color: const Color.fromRGBO(16, 185, 129, 1),
),
if (isActive)
SizedBox(width: 4.w),
Text(
label,
style: TextStyle(
......@@ -165,7 +175,6 @@ class _DeviceControl extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isOn = device.powerStatus == '1';
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
decoration: BoxDecoration(
......@@ -180,20 +189,23 @@ class _DeviceControl extends StatelessWidget {
size: 40.sp,
),
SizedBox(width: 16.w),
Text(
Expanded(
child: Text(
device.deviceName,
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(59, 130, 246, 1),
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
Text(
device.paramDisplayText.isNotEmpty ? device.paramDisplayText : device.powerStatusText,
style: TextStyle(
fontSize: 26.sp,
color: isOn
color: device.powerStatus == '1'
? const Color.fromRGBO(16, 185, 129, 1)
: const Color.fromRGBO(156, 163, 175, 1),
),
......
......@@ -97,23 +97,30 @@ class _RoomItem extends StatelessWidget {
room.roomNumber,
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w500,
color: _textColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (room.hasAlarmIcon)
Icon(Icons.warning_amber_outlined, color: _textColor, size: 20.sp),
if (!room.hasAlarmIcon)
Icon(Icons.warning_amber_outlined, color: _textColor, size: 24.sp),
Text(
room.statusText,
style: TextStyle(
fontSize: 20.sp,
fontSize: 24.sp,
color: _textColor,
),
),
],
),
],
),
);
}
}
......@@ -10,10 +10,10 @@ class StatusStats extends StatelessWidget {
@override
Widget build(BuildContext context) {
final items = [
_StatItemData(label: '入住', count: summary.occupiedCount, color: const Color(0xFF4A7CF7)),
_StatItemData(label: '空闲', count: summary.vacantCount, color: const Color(0xFF4CAF50)),
_StatItemData(label: '预定', count: summary.reservedCount, color: const Color(0xFF9C27B0)),
_StatItemData(label: '告警', count: summary.alarmCount, color: const Color(0xFFFF9800)),
_StatItemData(label: '入住', count: summary.occupiedCount, color: const Color.fromRGBO(73, 149, 234, 1)),
_StatItemData(label: '空闲', count: summary.vacantCount, color: const Color.fromRGBO(100, 116, 139, 1)),
_StatItemData(label: '预定', count: summary.reservedCount, color: const Color.fromRGBO(139, 193, 255, 1)),
_StatItemData(label: '告警', count: summary.alarmCount, color: const Color.fromRGBO(250, 204, 21, 1)),
];
return Container(
......@@ -24,7 +24,7 @@ class StatusStats extends StatelessWidget {
borderRadius: BorderRadius.circular(24.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 10.w,
children: items.map((item) => _StatItem(item: item)).toList(),
),
);
......@@ -45,13 +45,20 @@ class _StatItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
return Expanded(
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w),
decoration: BoxDecoration(
color: const Color.fromRGBO(229, 231, 235, 1),
borderRadius: BorderRadius.circular(24.r),
),
child: Column(
children: [
Text(
item.count.toString(),
style: TextStyle(
fontSize: 40.sp,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w600,
color: item.color,
),
),
......@@ -64,6 +71,8 @@ class _StatItem extends StatelessWidget {
),
),
],
),
)
);
}
}
......@@ -27,22 +27,22 @@ class RuleManagementCubit extends Cubit<RuleManagementState> {
/// 根据规则名称映射图标
IconData _mapIcon(String ruleName) {
final name = ruleName.toLowerCase();
if (name.contains('退房') || name.contains('logout')) {
return Icons.logout_outlined;
}
if (name.contains('清洁') || name.contains('灯') || name.contains('clean')) {
return Icons.cleaning_services_outlined;
}
if (name.contains('温度') || name.contains('thermostat')) {
return Icons.thermostat_outlined;
}
if (name.contains('深夜') || name.contains('night')) {
return Icons.nightlight_outlined;
}
if (name.contains('定时') || name.contains('巡检') || name.contains('time')) {
return Icons.access_time_outlined;
}
// final name = ruleName.toLowerCase();
// if (name.contains('退房') || name.contains('logout')) {
// return Icons.logout_outlined;
// }
// if (name.contains('清洁') || name.contains('灯') || name.contains('clean')) {
// return Icons.cleaning_services_outlined;
// }
// if (name.contains('温度') || name.contains('thermostat')) {
// return Icons.thermostat_outlined;
// }
// if (name.contains('深夜') || name.contains('night')) {
// return Icons.nightlight_outlined;
// }
// if (name.contains('定时') || name.contains('巡检') || name.contains('time')) {
// return Icons.access_time_outlined;
// }
return Icons.tune;
}
......
......@@ -51,7 +51,8 @@ class ReportRuleManagementView extends StatelessWidget {
itemCount: state.rules.length,
separatorBuilder: (context, index) => Divider(
height: 1.h,
color: const Color.fromRGBO(229, 231, 235, 1),
thickness: 0.5,
color: const Color.fromRGBO(100, 116, 139, 0.2),
indent: 28.w,
endIndent: 28.w,
),
......
......@@ -19,17 +19,16 @@ class RuleCard extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 28.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
),
child: Column(
children: [
Row(
child: Row(
children: [
Container(
width: 72.w,
height: 72.h,
width: 64.w,
height: 53.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(219, 234, 254, 1),
borderRadius: BorderRadius.circular(12.r),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
rule.icon,
......@@ -45,9 +44,9 @@ class RuleCard extends StatelessWidget {
Text(
rule.name,
style: TextStyle(
fontSize: 32.sp,
fontSize: 28.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(31, 41, 55, 1),
color: const Color.fromRGBO(0, 0, 0, 1),
),
),
SizedBox(height: 8.h),
......@@ -62,21 +61,21 @@ class RuleCard extends StatelessWidget {
),
),
FlutterSwitch(
width: 88.w,
height: 48.h,
// width: 64.w,
// height: 29.h,
width: 80.w,
height: 35.h,
valueFontSize: 20.sp,
toggleSize: 40.h,
value: rule.enabled,
borderRadius: 24.r,
toggleSize: 25.h,
borderRadius: 20.r,
padding: 4.w,
activeColor: const Color.fromRGBO(59, 130, 246, 1),
inactiveColor: const Color.fromRGBO(209, 213, 219, 1),
value: rule.enabled,
activeColor: const Color.fromRGBO(80, 162, 255, 1),
inactiveColor: const Color.fromRGBO(210, 213, 218, 1),
onToggle: (_) => onToggle(rule.id),
),
],
),
],
),
);
}
}
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