Commit 1244f87e authored by huqu's avatar huqu

fix bugs

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