Commit 1a461d59 authored by 张宏's avatar 张宏

2

parent 8e8f1b15
This diff is collapsed.
import 'package:equatable/equatable.dart';
int _parseInt(dynamic value, {int fallback = 0}) {
if (value == null) return fallback;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is String) {
final parsed = int.tryParse(value.trim());
return parsed ?? fallback;
}
return fallback;
}
String _parseString(dynamic value, {String fallback = ''}) {
if (value == null) return fallback;
if (value is String) return value;
return value.toString();
}
class DeviceSubtitleBO extends Equatable {
final String text;
final String voltage;
final String powerDisplay;
final String temperature;
final String statusText;
final String wifiSignal;
const DeviceSubtitleBO({
required this.text,
required this.voltage,
required this.powerDisplay,
required this.temperature,
required this.statusText,
required this.wifiSignal,
});
factory DeviceSubtitleBO.fromJson(Map<String, dynamic> json) {
return DeviceSubtitleBO(
text: _parseString(json['text']),
voltage: _parseString(json['voltage']),
powerDisplay: _parseString(json['powerDisplay']),
temperature: _parseString(json['temperature']),
statusText: _parseString(json['statusText']),
wifiSignal: _parseString(json['wifiSignal']),
);
}
@override
List<Object?> get props =>
[text, voltage, powerDisplay, temperature, statusText, wifiSignal];
}
class DeviceStatusTagBO extends Equatable {
final String text;
final String color;
const DeviceStatusTagBO({required this.text, required this.color});
factory DeviceStatusTagBO.fromJson(Map<String, dynamic> json) {
return DeviceStatusTagBO(
text: _parseString(json['text']),
color: _parseString(json['color']),
);
}
@override
List<Object?> get props => [text, color];
}
class DeviceRowBO extends Equatable {
final int deviceId;
final String deviceName;
final String deviceCode;
final String deviceLocation;
final String deviceTypeIcon;
final String deviceTypeName;
final DeviceSubtitleBO subtitle;
final DeviceStatusTagBO statusTag;
final String onlineStatus;
final String runStatus;
final int roomId;
final String roomNumber;
const DeviceRowBO({
required this.deviceId,
required this.deviceName,
required this.deviceCode,
required this.deviceLocation,
required this.deviceTypeIcon,
required this.deviceTypeName,
required this.subtitle,
required this.statusTag,
required this.onlineStatus,
required this.runStatus,
required this.roomId,
required this.roomNumber,
});
factory DeviceRowBO.fromJson(Map<String, dynamic> json) {
return DeviceRowBO(
deviceId: _parseInt(json['deviceId']),
deviceName: _parseString(json['deviceName']),
deviceCode: _parseString(json['deviceCode']),
deviceLocation: _parseString(json['deviceLocation']),
deviceTypeIcon: _parseString(json['deviceTypeIcon']),
deviceTypeName: _parseString(json['deviceTypeName']),
subtitle: DeviceSubtitleBO.fromJson(
json['subtitle'] as Map<String, dynamic>? ?? {}),
statusTag: DeviceStatusTagBO.fromJson(
json['statusTag'] as Map<String, dynamic>? ?? {}),
onlineStatus: _parseString(json['onlineStatus']),
runStatus: _parseString(json['runStatus']),
roomId: _parseInt(json['roomId']),
roomNumber: _parseString(json['roomNumber']),
);
}
@override
List<Object?> get props => [
deviceId,
deviceName,
deviceCode,
deviceLocation,
deviceTypeIcon,
deviceTypeName,
subtitle,
statusTag,
onlineStatus,
runStatus,
roomId,
roomNumber,
];
}
class DeviceListTabCountBO extends Equatable {
final int all;
final int airSwitch;
final int guestControl;
final int network;
const DeviceListTabCountBO({
required this.all,
required this.airSwitch,
required this.guestControl,
required this.network,
});
factory DeviceListTabCountBO.fromJson(Map<String, dynamic> json) {
return DeviceListTabCountBO(
all: _parseInt(json['all']),
airSwitch: _parseInt(json['airSwitch']),
guestControl: _parseInt(json['guestControl']),
network: _parseInt(json['network']),
);
}
@override
List<Object?> get props => [all, airSwitch, guestControl, network];
}
class DeviceListBO extends Equatable {
final int total;
final DeviceListTabCountBO tabCount;
final List<DeviceRowBO> rows;
const DeviceListBO({
required this.total,
required this.tabCount,
required this.rows,
});
factory DeviceListBO.fromJson(Map<String, dynamic> json) {
return DeviceListBO(
total: _parseInt(json['total']),
tabCount: DeviceListTabCountBO.fromJson(
json['tabCount'] as Map<String, dynamic>? ?? {}),
rows: (json['rows'] as List<dynamic>?)
?.map((e) => DeviceRowBO.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
@override
List<Object?> get props => [total, tabCount, rows];
}
import 'package:equatable/equatable.dart';
int _parseInt(dynamic value, {int fallback = 0}) {
if (value == null) return fallback;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is String) {
final parsed = int.tryParse(value.trim());
return parsed ?? fallback;
}
return fallback;
}
String _parseString(dynamic value, {String fallback = ''}) {
if (value == null) return fallback;
if (value is String) return value;
return value.toString();
}
/// GET /app/linkage/rules 响应中的单条规则
class LinkageRuleBO extends Equatable {
final int ruleId;
final String ruleName;
final String description;
final String enabled; // "true"/"false" 或 "1"/"0"
const LinkageRuleBO({
required this.ruleId,
required this.ruleName,
required this.description,
required this.enabled,
});
factory LinkageRuleBO.fromJson(Map<String, dynamic> json) {
return LinkageRuleBO(
ruleId: _parseInt(json['ruleId']),
ruleName: _parseString(json['ruleName']),
description: _parseString(json['description']),
enabled: _parseString(json['enabled']),
);
}
bool get isEnabled {
return enabled == 'true' || enabled == '1';
}
@override
List<Object?> get props => [ruleId, ruleName, description, enabled];
}
/// PUT /app/linkage/rules/toggle 响应
class LinkageRuleToggleBO extends Equatable {
final int ruleId;
final String enabled;
const LinkageRuleToggleBO({required this.ruleId, required this.enabled});
factory LinkageRuleToggleBO.fromJson(Map<String, dynamic> json) {
return LinkageRuleToggleBO(
ruleId: _parseInt(json['ruleId']),
enabled: _parseString(json['enabled']),
);
}
@override
List<Object?> get props => [ruleId, enabled];
}
import '../utils/http/response_model.dart';
import '../utils/http/dio_request.dart';
import '../models/bo/dashboard_bo.dart';
class DashboardRepository {
/// 获取经营概览看板数据
/// GET /app/dashboard/overview
Future<ResponseModel<DashboardOverviewBO>> getOverview() {
return DioRequest.instance.get<DashboardOverviewBO>(
'/app/dashboard/overview',
fromJsonT: (data) =>
DashboardOverviewBO.fromJson(data as Map<String, dynamic>),
);
}
/// 获取能耗分析数据
/// GET /app/dashboard/energy-analysis
Future<ResponseModel<EnergyAnalysisBO>> getEnergyAnalysis() {
return DioRequest.instance.get<EnergyAnalysisBO>(
'/app/dashboard/energy-analysis',
fromJsonT: (data) =>
EnergyAnalysisBO.fromJson(data as Map<String, dynamic>),
);
}
}
import '../utils/http/response_model.dart';
import '../utils/http/dio_request.dart';
import '../models/bo/device_list_bo.dart';
class DeviceListRepository {
/// 获取全部设备列表(分页)
/// GET /app/device/list
Future<ResponseModel<DeviceListBO>> getList({
required int pageSize,
required int pageNum,
int? deviceTypeId,
}) {
final params = <String, dynamic>{
'pageSize': pageSize,
'pageNum': pageNum,
};
if (deviceTypeId != null) {
params['deviceTypeId'] = deviceTypeId;
}
return DioRequest.instance.get<DeviceListBO>(
'/app/device/list',
queryParameters: params,
fromJsonT: (data) =>
DeviceListBO.fromJson(data as Map<String, dynamic>),
);
}
}
import '../utils/http/response_model.dart';
import '../utils/http/dio_request.dart';
import '../models/bo/linkage_rule_bo.dart';
class LinkageRuleRepository {
/// 获取联动规则列表
/// GET /app/linkage/rules
Future<ResponseModel<List<LinkageRuleBO>>> getRules() {
return DioRequest.instance.get<List<LinkageRuleBO>>(
'/app/linkage/rules',
fromJsonT: (data) {
final list = data as List<dynamic>;
return list
.map((e) => LinkageRuleBO.fromJson(e as Map<String, dynamic>))
.toList();
},
);
}
/// 启用/禁用联动规则
/// PUT /app/linkage/rules/toggle
Future<ResponseModel<LinkageRuleToggleBO>> toggleRule({
required int ruleId,
required String enabled,
}) {
return DioRequest.instance.put<LinkageRuleToggleBO>(
'/app/linkage/rules/toggle',
data: {'ruleId': ruleId, 'enabled': enabled},
fromJsonT: (data) =>
LinkageRuleToggleBO.fromJson(data as Map<String, dynamic>),
);
}
}
import '../models/bo/dashboard_bo.dart';
import '../repositories/dashboard_repository.dart';
class DashboardService {
final DashboardRepository _repository;
DashboardService({required DashboardRepository repository})
: _repository = repository;
/// 获取经营概览看板数据
Future<DashboardOverviewBO> getOverview() async {
final result = await _repository.getOverview();
if (result.success && result.data != null) {
return result.data!;
}
throw Exception(result.msg);
}
/// 获取能耗分析数据
Future<EnergyAnalysisBO> getEnergyAnalysis() async {
final result = await _repository.getEnergyAnalysis();
if (result.success && result.data != null) {
return result.data!;
}
throw Exception(result.msg);
}
}
import '../models/bo/device_list_bo.dart';
import '../repositories/device_list_repository.dart';
class DeviceListService {
final DeviceListRepository _repository;
DeviceListService({required DeviceListRepository repository})
: _repository = repository;
Future<DeviceListBO> getList({
required int pageSize,
required int pageNum,
int? deviceTypeId,
}) async {
final result = await _repository.getList(
pageSize: pageSize,
pageNum: pageNum,
deviceTypeId: deviceTypeId,
);
if (result.success && result.data != null) {
return result.data!;
}
throw Exception(result.msg);
}
}
import '../models/bo/linkage_rule_bo.dart';
import '../repositories/linkage_rule_repository.dart';
class LinkageRuleService {
final LinkageRuleRepository _repository;
LinkageRuleService({required LinkageRuleRepository repository})
: _repository = repository;
/// 获取联动规则列表
Future<List<LinkageRuleBO>> getRules() async {
final result = await _repository.getRules();
if (result.success && result.data != null) {
return result.data!;
}
throw Exception(result.msg);
}
/// 启用/禁用联动规则
Future<LinkageRuleToggleBO> toggleRule({
required int ruleId,
required String enabled,
}) async {
final result = await _repository.toggleRule(
ruleId: ruleId,
enabled: enabled,
);
if (result.success && result.data != null) {
return result.data!;
}
throw Exception(result.msg);
}
}
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:smart_hotel_app/repositories/device_list_repository.dart';
import 'package:smart_hotel_app/services/device_list_service.dart';
import 'package:smart_hotel_app/views/report/device/cubit/device_list_state.dart';
class DeviceListCubit extends Cubit<DeviceListState> {
DeviceListCubit() : super(const DeviceListState()) {
_initData();
static const int pageSize = 20;
final DeviceListService _service;
PagingController<int, DeviceInfo>? _pagingController;
DeviceListCubit({DeviceListService? service})
: _service = service ?? DeviceListService(repository: DeviceListRepository()),
super(const DeviceListState());
/// 绑定 PagingController,在 View 的 initState 中调用
void bindPagingController(PagingController<int, DeviceInfo> controller) {
_pagingController = controller;
}
void _initData() {
final devices = [
const DeviceInfo(
id: '1',
name: '智能空开 1F-01',
room: '101',
type: DeviceTypeEnum.smartBreaker,
online: true,
status: '正常',
lastUpdate: '10:30',
),
const DeviceInfo(
id: '2',
name: '客控设备 1F-01',
room: '101',
type: DeviceTypeEnum.guestControl,
online: true,
status: '正常',
lastUpdate: '10:32',
),
const DeviceInfo(
id: '3',
name: '网络设备 1F',
room: '一楼',
type: DeviceTypeEnum.network,
online: true,
status: '正常',
lastUpdate: '10:28',
),
const DeviceInfo(
id: '4',
name: '智能空开 2F-01',
room: '201',
type: DeviceTypeEnum.smartBreaker,
online: true,
status: '正常',
lastUpdate: '10:25',
),
const DeviceInfo(
id: '5',
name: '客控设备 2F-01',
room: '201',
type: DeviceTypeEnum.guestControl,
online: false,
status: '离线',
lastUpdate: '09:15',
),
const DeviceInfo(
id: '6',
name: '智能空开 3F-01',
room: '301',
type: DeviceTypeEnum.smartBreaker,
online: true,
status: '正常',
lastUpdate: '10:30',
),
const DeviceInfo(
id: '7',
name: '客控设备 3F-01',
room: '301',
type: DeviceTypeEnum.guestControl,
online: true,
status: '正常',
lastUpdate: '10:30',
),
const DeviceInfo(
id: '8',
name: '网络设备 2F',
room: '二楼',
type: DeviceTypeEnum.network,
online: true,
status: '正常',
lastUpdate: '10:30',
),
];
/// 解绑 PagingController,在 View 的 dispose 中调用
void unbindPagingController() {
_pagingController = null;
}
/// 供 PagingController.fetchPage 回调使用
Future<List<DeviceInfo>> fetchPage(int pageKey) async {
final typeId = deviceTypeIdFromEnum(state.selectedType);
final result = await _service.getList(
pageSize: pageSize,
pageNum: pageKey,
deviceTypeId: typeId,
);
// 首页时更新 tab 计数和总数
if (pageKey == 1) {
emit(state.copyWith(
tabAll: result.tabCount.all,
tabAirSwitch: result.tabCount.airSwitch,
tabGuestControl: result.tabCount.guestControl,
tabNetwork: result.tabCount.network,
totalCount: result.total,
));
}
emit(state.copyWith(devices: devices));
return result.rows.map((row) {
return DeviceInfo(
deviceId: row.deviceId,
name: row.deviceName,
room: row.roomNumber.isNotEmpty ? row.roomNumber : row.deviceLocation,
type: deviceTypeFromName(row.deviceTypeName),
online: row.onlineStatus == '1',
status: row.statusTag.text.isNotEmpty
? row.statusTag.text
: (row.onlineStatus == '1' ? '正常' : '离线'),
lastUpdate: row.subtitle.text,
);
}).toList();
}
/// 切换筛选类型
void selectType(DeviceTypeEnum type) {
if (state.selectedType == type) return;
emit(state.copyWith(selectedType: type));
_pagingController?.refresh();
}
}
......@@ -7,8 +7,37 @@ enum DeviceTypeEnum {
network,
}
class DeviceInfo {
final String id;
/// deviceTypeId 映射: all→null, smartBreaker→1, guestControl→2, network→3
int? deviceTypeIdFromEnum(DeviceTypeEnum type) {
switch (type) {
case DeviceTypeEnum.all:
return null;
case DeviceTypeEnum.smartBreaker:
return 1;
case DeviceTypeEnum.guestControl:
return 2;
case DeviceTypeEnum.network:
return 3;
}
}
/// 根据 API 的 deviceTypeName 匹配 DeviceTypeEnum
DeviceTypeEnum deviceTypeFromName(String typeName) {
final name = typeName.toLowerCase();
if (name.contains('空开') || name.contains('airswitch') || name.contains('breaker')) {
return DeviceTypeEnum.smartBreaker;
}
if (name.contains('客控') || name.contains('guest')) {
return DeviceTypeEnum.guestControl;
}
if (name.contains('网络') || name.contains('network')) {
return DeviceTypeEnum.network;
}
return DeviceTypeEnum.smartBreaker; // fallback
}
class DeviceInfo extends Equatable {
final int deviceId;
final String name;
final String room;
final DeviceTypeEnum type;
......@@ -17,62 +46,72 @@ class DeviceInfo {
final String lastUpdate;
const DeviceInfo({
required this.id,
required this.name,
required this.room,
this.deviceId = 0,
this.name = '',
this.room = '',
required this.type,
required this.online,
required this.status,
required this.lastUpdate,
this.online = false,
this.status = '',
this.lastUpdate = '',
});
DeviceInfo copyWith({
String? id,
String? name,
String? room,
DeviceTypeEnum? type,
bool? online,
String? status,
String? lastUpdate,
}) {
return DeviceInfo(
id: id ?? this.id,
name: name ?? this.name,
room: room ?? this.room,
type: type ?? this.type,
online: online ?? this.online,
status: status ?? this.status,
lastUpdate: lastUpdate ?? this.lastUpdate,
);
}
@override
List<Object?> get props => [deviceId, name, room, type, online, status, lastUpdate];
}
class DeviceListState extends Equatable {
final List<DeviceInfo> devices;
final bool isLoading;
final String? error;
final int tabAll;
final int tabAirSwitch;
final int tabGuestControl;
final int tabNetwork;
final int totalCount;
final DeviceTypeEnum selectedType;
const DeviceListState({
this.devices = const [],
this.isLoading = false,
this.error,
this.tabAll = 0,
this.tabAirSwitch = 0,
this.tabGuestControl = 0,
this.tabNetwork = 0,
this.totalCount = 0,
this.selectedType = DeviceTypeEnum.all,
});
DeviceListState copyWith({
List<DeviceInfo>? devices,
bool? isLoading,
String? error,
int? tabAll,
int? tabAirSwitch,
int? tabGuestControl,
int? tabNetwork,
int? totalCount,
DeviceTypeEnum? selectedType,
bool clearError = false,
}) {
return DeviceListState(
devices: devices ?? this.devices,
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
tabAll: tabAll ?? this.tabAll,
tabAirSwitch: tabAirSwitch ?? this.tabAirSwitch,
tabGuestControl: tabGuestControl ?? this.tabGuestControl,
tabNetwork: tabNetwork ?? this.tabNetwork,
totalCount: totalCount ?? this.totalCount,
selectedType: selectedType ?? this.selectedType,
);
}
List<DeviceInfo> get filteredDevices {
if (selectedType == DeviceTypeEnum.all) {
return devices;
}
return devices.where((device) => device.type == selectedType).toList();
}
@override
List<Object?> get props => [devices, selectedType];
List<Object?> get props => [
isLoading,
error,
tabAll,
tabAirSwitch,
tabGuestControl,
tabNetwork,
totalCount,
selectedType,
];
}
......@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:smart_hotel_app/views/report/device/cubit/device_list_cubit.dart';
import 'package:smart_hotel_app/views/report/device/cubit/device_list_state.dart';
import 'package:smart_hotel_app/views/report/device/widget/device_card.dart';
......@@ -15,7 +16,52 @@ class ReportDeviceListView extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => DeviceListCubit(),
child: BlocBuilder<DeviceListCubit, DeviceListState>(
child: const _ReportDeviceListBody(),
);
}
}
class _ReportDeviceListBody extends StatefulWidget {
const _ReportDeviceListBody();
@override
State<_ReportDeviceListBody> createState() => _ReportDeviceListBodyState();
}
class _ReportDeviceListBodyState extends State<_ReportDeviceListBody> {
late final PagingController<int, DeviceInfo> _pagingController;
late final DeviceListCubit _cubit;
@override
void initState() {
super.initState();
_cubit = context.read<DeviceListCubit>();
_pagingController = PagingController<int, DeviceInfo>(
getNextPageKey: (state) {
if (state.keys == null || state.keys!.isEmpty) return 1;
final total = _cubit.state.totalCount;
final nextKey = state.nextIntPageKey;
if (nextKey == null) return null;
if ((nextKey - 1) * DeviceListCubit.pageSize >= total) {
return null;
}
return nextKey;
},
fetchPage: (pageKey) => _cubit.fetchPage(pageKey),
);
_cubit.bindPagingController(_pagingController);
}
@override
void dispose() {
_pagingController.dispose();
_cubit.unbindPagingController();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DeviceListCubit, DeviceListState>(
builder: (context, state) {
return Scaffold(
backgroundColor: const Color.fromRGBO(242, 243, 245, 1),
......@@ -32,13 +78,18 @@ class ReportDeviceListView extends StatelessWidget {
'全部设备',
style: TextStyle(
color: Colors.black,
fontSize: 18.sp,
fontSize: 32.sp,
fontWeight: FontWeight.bold,
),
),
centerTitle: true,
),
body: SingleChildScrollView(
body: PagingListener<int, DeviceInfo>(
controller: _pagingController,
builder: (context, pagingState, fetchNextPage) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: Column(
......@@ -47,24 +98,95 @@ class ReportDeviceListView extends StatelessWidget {
DeviceFilterTab(
selectedType: state.selectedType,
onSelect: (type) {
context.read<DeviceListCubit>().selectType(type);
context
.read<DeviceListCubit>()
.selectType(type);
},
),
SizedBox(height: 10.h),
...state.filteredDevices.map((device) {
],
),
),
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 28.w),
sliver: PagedSliverList<int, DeviceInfo>(
state: pagingState,
fetchNextPage: fetchNextPage,
builderDelegate:
PagedChildBuilderDelegate<DeviceInfo>(
firstPageErrorIndicatorBuilder: (_) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 60.h),
Text(
'加载失败: ${pagingState.error}',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
textAlign: TextAlign.center,
),
SizedBox(height: 24.h),
ElevatedButton(
onPressed: fetchNextPage,
child: Text('重新加载',
style: TextStyle(fontSize: 28.sp)),
),
],
),
),
noItemsFoundIndicatorBuilder: (_) => Padding(
padding: EdgeInsets.only(top: 60.h),
child: Text(
'暂无设备',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
),
newPageErrorIndicatorBuilder: (_) => Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20.h),
child: Column(
children: [
Text(
'加载失败: ${pagingState.error}',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
SizedBox(height: 12.h),
TextButton(
onPressed: fetchNextPage,
child: Text('重试',
style: TextStyle(fontSize: 24.sp)),
),
],
),
),
),
itemBuilder: (_, item, __) {
return Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: DeviceCard(device: device),
child: DeviceCard(device: item),
);
}).toList(),
SizedBox(height: 10.h),
],
},
),
),
),
SliverToBoxAdapter(
child: SizedBox(height: 20.h),
),
],
);
},
),
);
},
);
}
}
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/models/bo/dashboard_bo.dart';
import 'package:smart_hotel_app/repositories/dashboard_repository.dart';
import 'package:smart_hotel_app/services/dashboard_service.dart';
import 'package:smart_hotel_app/views/report/energy/cubit/energy_state.dart';
class EnergyCubit extends Cubit<EnergyState> {
EnergyCubit() : super(const EnergyState());
final DashboardService _service;
EnergyCubit({DashboardService? service})
: _service = service ?? DashboardService(repository: DashboardRepository()),
super(const EnergyState()) {
loadData();
}
Future<void> loadData() async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
final data = await _service.getEnergyAnalysis();
emit(_mapAnalysisToState(data));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
EnergyState _mapAnalysisToState(EnergyAnalysisBO data) {
// 计算今日变化百分比
double todayChange = 0;
if (data.yesterdayElectricity > 0) {
todayChange =
((data.todayElectricity - data.yesterdayElectricity) /
data.yesterdayElectricity) *
100;
}
// 计算本月变化百分比
double monthChange = 0;
if (data.lastMonthElectricity > 0) {
monthChange =
((data.monthElectricity - data.lastMonthElectricity) /
data.lastMonthElectricity) *
100;
}
// 将逐小时数据点提取为 List<double>
final hourlyData = data.hourlyUsageToday.hourlyPoints
.map((p) => p.value)
.toList();
// 将分区数据映射为 ZoneData 列表
final totalToday = data.zoneUsageRatio.totalToday;
final zoneData = data.zoneUsageRatio.zones.map((z) {
final percent = totalToday > 0 ? (z.todayKwh / totalToday * 100) : 0.0;
return ZoneData(
name: z.zoneLabel,
percentage: percent,
energy: z.todayKwh,
);
}).toList();
return EnergyState(
isLoading: false,
todayEnergy: data.todayElectricity,
todayEnergyChange: todayChange,
monthEnergy: data.monthElectricity,
monthEnergyChange: monthChange,
monthBill: data.monthCostEstimate,
hourlyData: hourlyData,
zoneData: zoneData,
);
}
}
import 'package:equatable/equatable.dart';
class ZoneData {
class ZoneData extends Equatable {
final String name;
final double percentage;
final double energy;
......@@ -10,9 +10,14 @@ class ZoneData {
required this.percentage,
required this.energy,
});
@override
List<Object?> get props => [name, percentage, energy];
}
class EnergyState extends Equatable {
final bool isLoading;
final String? error;
final double todayEnergy;
final double todayEnergyChange;
final double monthEnergy;
......@@ -22,6 +27,8 @@ class EnergyState extends Equatable {
final List<ZoneData> zoneData;
const EnergyState({
this.isLoading = false,
this.error,
this.todayEnergy = 1284,
this.todayEnergyChange = -8,
this.monthEnergy = 38420,
......@@ -40,6 +47,8 @@ class EnergyState extends Equatable {
});
EnergyState copyWith({
bool? isLoading,
String? error,
double? todayEnergy,
double? todayEnergyChange,
double? monthEnergy,
......@@ -47,8 +56,11 @@ class EnergyState extends Equatable {
double? monthBill,
List<double>? hourlyData,
List<ZoneData>? zoneData,
bool clearError = false,
}) {
return EnergyState(
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
todayEnergy: todayEnergy ?? this.todayEnergy,
todayEnergyChange: todayEnergyChange ?? this.todayEnergyChange,
monthEnergy: monthEnergy ?? this.monthEnergy,
......@@ -61,6 +73,8 @@ class EnergyState extends Equatable {
@override
List<Object?> get props => [
isLoading,
error,
todayEnergy,
todayEnergyChange,
monthEnergy,
......
......@@ -37,10 +37,35 @@ class ReportEnergyDetailView extends StatelessWidget {
),
centerTitle: true,
),
body: SingleChildScrollView(
child: BlocBuilder<EnergyCubit, EnergyState>(
body: BlocBuilder<EnergyCubit, EnergyState>(
builder: (context, state) {
return Column(
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'加载失败: ${state.error}',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () =>
context.read<EnergyCubit>().loadData(),
child: const Text('重试'),
),
],
),
);
}
return SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 16.h),
EnergyStatsCardBlock(
......@@ -56,11 +81,11 @@ class ReportEnergyDetailView extends StatelessWidget {
ZoneChartBlock(zoneData: state.zoneData),
SizedBox(height: 20.h),
],
),
);
},
),
),
),
);
}
}
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/models/bo/dashboard_bo.dart';
import 'package:smart_hotel_app/repositories/dashboard_repository.dart';
import 'package:smart_hotel_app/services/dashboard_service.dart';
import 'package:smart_hotel_app/views/report/index/cubit/report_index_state.dart';
class ReportIndexCubit extends Cubit<ReportIndexState> {
ReportIndexCubit() : super(const ReportIndexState()) {
initData();
final DashboardService _service;
ReportIndexCubit({DashboardService? service})
: _service = service ?? DashboardService(repository: DashboardRepository()),
super(const ReportIndexState()) {
loadData();
}
void initData() {
emit(state.copyWith(
Future<void> loadData() async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
final overview = await _service.getOverview();
emit(_mapOverviewToState(overview));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
ReportIndexState _mapOverviewToState(DashboardOverviewBO overview) {
// 今日用电指标
final elec = overview.electricityToday;
final yesterdayVal = elec.yesterdayValue;
final todayVal = elec.value;
double changePercent = 0;
if (yesterdayVal > 0) {
changePercent = ((todayVal - yesterdayVal) / yesterdayVal * 100);
}
final isDown = changePercent <= 0;
final changeAbs = changePercent.abs().round();
return state.copyWith(
isLoading: false,
reportHeader: {
'title': '经营概览看板',
'subtitle': '管理层视角 · 关键指标一览',
},
reportMetrics: {
'powerUsage': {
'value': 1284,
'value': todayVal.toInt(),
'unit': 'kWh',
'change': -8,
'changeText': '↓ 8%',
'progress': 0.6,
'change': isDown ? -changeAbs : changeAbs,
'changeText': isDown ? '↓ $changeAbs%' : '↑ $changeAbs%',
'progress': (todayVal / 2000).clamp(0.0, 1.0),
},
'pendingAlerts': {
'value': 3,
'value': overview.alertPending.count,
'unit': '条',
'change': null,
'changeText': '需关注',
'progress': 0.3,
'changeText': overview.alertPending.count > 0 ? '需关注' : '无告警',
'progress': (overview.alertPending.count / 10).clamp(0.0, 1.0),
},
},
weeklyPowerUsage: {
'title': '本周用电趋势',
'unit': 'kWh',
'dateRange': '5.7 - 5.13',
'data': [
{'day': '一', 'value': 120},
{'day': '二', 'value': 180},
{'day': '三', 'value': 150},
{'day': '四', 'value': 200},
{'day': '五', 'value': 250},
{'day': '六', 'value': 220},
{'day': '日', 'value': 164},
],
'dateRange': overview.electricityTrend.dateRange,
'data': _mapWeekDays(overview.electricityTrend.points),
},
roomStatusDistribution: {
'title': '客房状态分布',
'totalRooms': 52,
'statuses': [
{'name': '入住中', 'count': 45, 'color': const Color.fromRGBO(59, 130, 246, 1)},
{'name': '空闲', 'count': 7, 'color': const Color.fromRGBO(148, 163, 184, 1)},
{'name': '已预订', 'count': 5, 'color': const Color.fromRGBO(96, 165, 250, 1)},
{'name': '设备告警', 'count': 3, 'color': const Color.fromRGBO(249, 115, 22, 1)},
],
'totalRooms': overview.roomStatusDistribution.total,
'statuses': _mapRoomStatuses(overview.roomStatusDistribution.items),
},
deviceOnlineRate: {
'title': '设备在线率',
'rate': 1.0,
'rateText': '100%',
'devices': [
{'name': '智能空开', 'count': 16},
{'name': '客控设备', 'count': 24},
{'name': '网络设备', 'count': 8},
],
'rateText': '${overview.deviceTypeDistribution.totalDeviceCount}台',
'devices': _mapDeviceTypes(overview.deviceTypeDistribution.items),
},
quickActions: {
'title': '快捷操作',
......@@ -70,6 +82,48 @@ class ReportIndexCubit extends Cubit<ReportIndexState> {
{'name': '联动规则', 'icon': Icons.tune},
],
},
));
);
}
/// 将趋势点映射为周几标签
List<Map<String, dynamic>> _mapWeekDays(List<DashboardTrendPointBO> points) {
const dayLabels = ['一', '二', '三', '四', '五', '六', '日'];
return points.asMap().entries.map((entry) {
final index = entry.key;
final point = entry.value;
return {
'day': index < dayLabels.length ? dayLabels[index] : '${index + 1}',
'value': point.value.toInt(),
};
}).toList();
}
/// 映射客房状态并分配颜色
List<Map<String, dynamic>> _mapRoomStatuses(List<RoomStatusItemBO> items) {
const statusColors = [
Color.fromRGBO(59, 130, 246, 1),
Color.fromRGBO(148, 163, 184, 1),
Color.fromRGBO(96, 165, 250, 1),
Color.fromRGBO(249, 115, 22, 1),
];
return items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return {
'name': item.statusLabel,
'count': item.count,
'color': statusColors[index % statusColors.length],
};
}).toList();
}
/// 映射设备类型分布
List<Map<String, dynamic>> _mapDeviceTypes(List<DeviceTypeItemBO> items) {
return items.map((item) {
return {
'name': item.typeLabel,
'count': item.count,
};
}).toList();
}
}
import 'package:equatable/equatable.dart';
class ReportIndexState extends Equatable {
final bool isLoading;
final String? error;
final Map<String, dynamic> reportHeader;
final Map<String, dynamic> reportMetrics;
final Map<String, dynamic> weeklyPowerUsage;
......@@ -9,6 +11,8 @@ class ReportIndexState extends Equatable {
final Map<String, dynamic> quickActions;
const ReportIndexState({
this.isLoading = false,
this.error,
this.reportHeader = const {},
this.reportMetrics = const {},
this.weeklyPowerUsage = const {},
......@@ -18,14 +22,19 @@ class ReportIndexState extends Equatable {
});
ReportIndexState copyWith({
bool? isLoading,
String? error,
Map<String, dynamic>? reportHeader,
Map<String, dynamic>? reportMetrics,
Map<String, dynamic>? weeklyPowerUsage,
Map<String, dynamic>? roomStatusDistribution,
Map<String, dynamic>? deviceOnlineRate,
Map<String, dynamic>? quickActions,
bool clearError = false,
}) {
return ReportIndexState(
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
reportHeader: reportHeader ?? this.reportHeader,
reportMetrics: reportMetrics ?? this.reportMetrics,
weeklyPowerUsage: weeklyPowerUsage ?? this.weeklyPowerUsage,
......@@ -37,6 +46,8 @@ class ReportIndexState extends Equatable {
@override
List<Object?> get props => [
isLoading,
error,
reportHeader,
reportMetrics,
weeklyPowerUsage,
......
......@@ -21,6 +21,37 @@ class ReportIndexView extends StatelessWidget {
create: (_) => ReportIndexCubit(),
child: BlocBuilder<ReportIndexCubit, ReportIndexState>(
builder: (context, state) {
if (state.isLoading) {
return Container(
color: const Color.fromRGBO(242, 243, 245, 1),
child: const Center(child: CircularProgressIndicator()),
);
}
if (state.error != null) {
return Container(
color: const Color.fromRGBO(242, 243, 245, 1),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'加载失败: ${state.error}',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () =>
context.read<ReportIndexCubit>().loadData(),
child: const Text('重试'),
),
],
),
),
);
}
return LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
......@@ -31,7 +62,10 @@ class ReportIndexView extends StatelessWidget {
child: Container(
width: double.infinity,
color: const Color.fromRGBO(242, 243, 245, 1),
padding: EdgeInsets.only(left: 28.w, right: 28.w, top: ScreenUtil().statusBarHeight),
padding: EdgeInsets.only(
left: 28.w,
right: 28.w,
top: ScreenUtil().statusBarHeight),
child: Column(
children: [
ReportHeaderBlock(
......@@ -50,7 +84,8 @@ class ReportIndexView extends StatelessWidget {
DeviceOnlineRate(
deviceOnlineRate: state.deviceOnlineRate),
SizedBox(height: 10.h),
QuickActionsBlock(quickActions: state.quickActions),
QuickActionsBlock(
quickActions: state.quickActions),
SizedBox(height: 10.h),
],
),
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/models/bo/linkage_rule_bo.dart';
import 'package:smart_hotel_app/repositories/linkage_rule_repository.dart';
import 'package:smart_hotel_app/services/linkage_rule_service.dart';
import 'package:smart_hotel_app/views/report/rule/cubit/rule_management_state.dart';
class RuleManagementCubit extends Cubit<RuleManagementState> {
RuleManagementCubit() : super(const RuleManagementState()) {
_initData();
final LinkageRuleService _service;
RuleManagementCubit({LinkageRuleService? service})
: _service = service ??
LinkageRuleService(repository: LinkageRuleRepository()),
super(const RuleManagementState()) {
loadRules();
}
void _initData() {
final rules = [
const RuleInfo(
id: '1',
name: '退房自动断电',
description: '酒管系统—酒度状态',
enabled: true,
triggerCondition: '酒管系统—酒度状态',
action: '断电',
icon: Icons.logout_outlined,
),
const RuleInfo(
id: '2',
name: '清洁中请自动开灯',
description: '服务彩忧消洁中调',
enabled: true,
triggerCondition: '服务彩忧消洁中调',
action: '开灯',
icon: Icons.cleaning_services_outlined,
),
const RuleInfo(
id: '3',
name: '温度过高自动保护',
description: '温度·80°C—断电+告管',
enabled: true,
triggerCondition: '温度·80°C',
action: '断电+告管',
icon: Icons.thermostat_outlined,
),
const RuleInfo(
id: '4',
name: '深夜低功事休眠',
description: '23:00-06:00 平&lt;50W',
enabled: false,
triggerCondition: '23:00-06:00 平&lt;50W',
action: '休眠',
icon: Icons.nightlight_outlined,
),
const RuleInfo(
id: '5',
name: '定时全楼巡检提醒',
description: '国日 09:00/15.00',
enabled: true,
triggerCondition: '国日 09:00/15.00',
action: '提醒',
icon: Icons.access_time_outlined,
),
];
Future<void> loadRules() async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
final rules = await _service.getRules();
emit(state.copyWith(isLoading: false, rules: rules.map(_toRuleInfo).toList()));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
emit(state.copyWith(rules: rules));
/// 根据规则名称映射图标
IconData _mapIcon(String ruleName) {
final name = ruleName.toLowerCase();
if (name.contains('退房') || name.contains('logout')) {
return Icons.logout_outlined;
}
if (name.contains('清洁') || name.contains('灯') || name.contains('clean')) {
return Icons.cleaning_services_outlined;
}
if (name.contains('温度') || name.contains('thermostat')) {
return Icons.thermostat_outlined;
}
if (name.contains('深夜') || name.contains('night')) {
return Icons.nightlight_outlined;
}
if (name.contains('定时') || name.contains('巡检') || name.contains('time')) {
return Icons.access_time_outlined;
}
return Icons.tune;
}
void toggleRule(String id) {
final updatedRules = state.rules.map((rule) {
if (rule.id == id) {
return rule.copyWith(enabled: !rule.enabled);
RuleInfo _toRuleInfo(LinkageRuleBO bo) {
return RuleInfo(
ruleId: bo.ruleId,
name: bo.ruleName,
description: bo.description,
enabled: bo.isEnabled,
icon: _mapIcon(bo.ruleName),
);
}
return rule;
}).toList();
emit(state.copyWith(rules: updatedRules));
/// 启用/禁用联动规则(View 层传入 String id)
Future<void> toggleRule(String id) async {
final ruleId = int.tryParse(id) ?? 0;
if (ruleId == 0) return;
if (state.isToggling) return; // 防重复提交
final previousRules = List<RuleInfo>.from(state.rules);
final index = state.rules.indexWhere((r) => r.ruleId == ruleId);
if (index == -1) return;
final currentEnabled = state.rules[index].enabled;
final newEnabled = !currentEnabled;
// 乐观更新 UI
final updatedRules = List<RuleInfo>.from(state.rules);
updatedRules[index] = updatedRules[index].copyWith(enabled: newEnabled);
emit(state.copyWith(
rules: updatedRules,
isToggling: true,
togglingRuleId: ruleId,
));
try {
await _service.toggleRule(
ruleId: ruleId,
enabled: newEnabled ? '1' : '0',
);
emit(state.copyWith(isToggling: false));
} catch (e) {
// 失败回滚
emit(state.copyWith(
rules: previousRules,
isToggling: false,
error: e.toString(),
));
}
}
}
import 'package:flutter/material.dart';
import 'package:equatable/equatable.dart';
class RuleInfo {
final String id;
class RuleInfo extends Equatable {
final int ruleId;
final String name;
final String description;
final bool enabled;
final String triggerCondition;
final String action;
final IconData icon;
const RuleInfo({
required this.id,
required this.name,
required this.description,
required this.enabled,
required this.triggerCondition,
required this.action,
required this.icon,
this.ruleId = 0,
this.name = '',
this.description = '',
this.enabled = false,
this.icon = Icons.tune,
});
RuleInfo copyWith({
String? id,
int? ruleId,
String? name,
String? description,
bool? enabled,
String? triggerCondition,
String? action,
IconData? icon,
}) {
return RuleInfo(
id: id ?? this.id,
ruleId: ruleId ?? this.ruleId,
name: name ?? this.name,
description: description ?? this.description,
enabled: enabled ?? this.enabled,
triggerCondition: triggerCondition ?? this.triggerCondition,
action: action ?? this.action,
icon: icon ?? this.icon,
);
}
/// 兼容 View 层 rule.id 引用,返回 ruleId 的字符串形式
String get id => ruleId.toString();
@override
List<Object?> get props => [ruleId, name, description, enabled, icon];
}
class RuleManagementState extends Equatable {
final bool isLoading;
final String? error;
final bool isToggling; // 某个开关操作进行中
final int togglingRuleId; // 正在操作的规则ID
final List<RuleInfo> rules;
const RuleManagementState({
this.isLoading = false,
this.error,
this.isToggling = false,
this.togglingRuleId = 0,
this.rules = const [],
});
RuleManagementState copyWith({
bool? isLoading,
String? error,
bool? isToggling,
int? togglingRuleId,
List<RuleInfo>? rules,
bool clearError = false,
}) {
return RuleManagementState(
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
isToggling: isToggling ?? this.isToggling,
togglingRuleId: togglingRuleId ?? this.togglingRuleId,
rules: rules ?? this.rules,
);
}
@override
List<Object?> get props => [rules];
List<Object?> get props => [
isLoading,
error,
isToggling,
togglingRuleId,
rules,
];
}
# 智慧酒店 App 接口对接方案
# 智慧酒店 App 接口对接方案
......@@ -222,10 +222,18 @@ class xxxBO extends Equatable {
| 客房详情-房间总电源-switch开 |POST | /app/room/power/on | 已对接 | 客房详情-房间总电源-switch开 | |
| 客房详情-房间总电源-switch关 |POST | /app/room/power/off | 已对接 | 客房详情-房间总电源-switch关 | |
| 空调控制 |GET | /app/device/ac/status | 对接 | 空调控制 | |
| 空调控制-相关操作 |POST | /app/device/ac/control | 对接 | 空调控制-相关操作 | |
| 空调控制 |GET | /app/device/ac/status | 对接 | 空调控制 | |
| 空调控制-相关操作 |POST | /app/device/ac/control | 对接 | 空调控制-相关操作 | |
| 经营概览看板 |GET | /app/dashboard/overview | 已对接 | 经营概览看板 | |
| 能耗分析 |GET | /app/dashboard/energy-analysis | 已对接 | 能耗分析 | |
| 全部设备 |GET | /app/device/list | 已对接 | 全部设备 | |
| 联动规则管理 |GET | /app/linkage/rules | 未对接 | 联动规则管理 | |
| 联动规则管理-启用/禁用 |PUT | /app/linkage/rules/toggle | 未对接 | 联动规则管理-启用/禁用 | |
......@@ -233,6 +241,210 @@ class xxxBO extends Equatable {
### 3.2 接口详细定义
#### 联动规则管理-启用/禁用
**PUT /app/linkage/rules/toggle**
```
求体:
{
"ruleId": 0,
"enabled": "string"
}
应 data:
{
"code": 0,
"msg": "string",
"data": {
"ruleId": 0,
"enabled": "string"
}
}
```
#### 联动规则管理
**GET /app/linkage/rules**
```
求体:
{
}
应 data:
{
"code": 0,
"msg": "string",
"data": [
{
"ruleId": 0,
"ruleName": "string",
"description": "string",
"enabled": "string" // 启用/禁用
}
]
}
```
#### 全部设备
**GET /app/device/list**
```
求体:
{
"pageSize": 20,
"pageNum": 1,
"deviceTypeId":1 // 设备类型id 可选
}
应 data:
{
"code": 0,
"msg": "string",
"data": {
"total": 0, // 总记录数,满足条件的设备总数,用于前端分页计算
"tabCount": {
"all": 0, // 全部设备数量
"airSwitch": 0, // 空开类设备数
"guestControl": 0, // 客控类设备数
"network": 0 // 网络类设备数
},
"rows": [ // 设备行记录列表
{
"deviceId": 0,
"deviceName": "string",
"deviceCode": "string",
"deviceLocation": "string",
"deviceTypeIcon": "string",
"deviceTypeName": "string",
"subtitle": {
"text": "string",
"voltage": "string",
"powerDisplay": "string",
"temperature": "string",
"statusText": "string",
"wifiSignal": "string"
},
"statusTag": { // 右侧状态标签,颜色+文字组合,来源于 hotel_device.run_status 字段判断:normal→绿色'正常'/绿色'在线' warning→黄色'注意' fault→红色'故障' offline→灰色'离线',对应卡片右侧的彩色标签(告警黄/正常绿/在线绿/注意黄/故障红)
"text": "string", // 状态标签文字:'正常'/'在线'/'告警'/'注意'/'故障'/'离线',根据 run_status + online_status 综合判断
"color": "string" // 状态标签颜色类型:success=绿色(green) warning=黄色(orange/yellow) danger=红色(red) info=灰色(gray),用于前端渲染标签背景色
},
"onlineStatus": "string", // 在线状态码:0=离线 1=在线
"runStatus": "string", // 运行状态码:normal=正常 warning=预警/注意 fault=异常/故障 offline=离线
"roomId": 0,
"roomNumber": "string"
}
]
}
}
```
#### 能耗分析
**GET /app/dashboard/energy-analysis**
```
求体:
{
}
应 data:
{
"code": 0,
"msg": "string",
"data": {
"todayElectricity": 0, // 今日累计用电量(kWh)
"yesterdayElectricity": 0,
"monthElectricity": 0,
"lastMonthElectricity": 0,
"monthCostEstimate": 0,
"hourlyUsageToday": { // 今日逐时用电柱状图
"maxValue": 0, // 【计算字段 | 来源: 24个 hourlyPoints 的 value 值 | 取值方式: MAX(hourlyPoints[].value),即24个小时中最大的那个 power_consumption 累加值 | 说明: 当天24小时中单小时最大用电量值(kWh),用于设定 Y 轴上限(向上取整到十位整数如 80/90/100),避免柱子贴顶】
"hourlyPoints": [ // 逐小时数据点列表
{
"time": "string",
"value": 0
}
]
},
"zoneUsageRatio": { // 分区用电占比(横向条形图)
"totalToday": 0,
"totalYesterday": 0,
"zones": [ // 各区域用电明细列表
{
"zoneLabel": "string",
"todayKwh": 0,
"yesterdayKwh": 0
}
]
}
}
}
```
#### 经营概览看板
**GET /app/dashboard/overview**
```
求体:
{
}
应 data:
{
"code": 0,
"msg": "string",
"data": {
"electricityToday": { // 今日用电统计数据
"value": 0,
"yesterdayValue": 0
},
"alertPending": { // 待处理告警卡片数据
"count": 0
},
"electricityTrend": { // 本周用电趋势折线图
"dateRange": "string",
"maxValue": 0,
"points": [
{
"time": "string",
"value": 0
}
]
},
"roomStatusDistribution": { // 客房状态分布(横向条形图)
"total": 0,
"items": [
{
"statusLabel": "string",
"count": 0,
"percent": 0
}
]
},
"deviceTypeDistribution": { // 设备类型分布统计(圆环图)
"totalDeviceCount": 0,
"items": [
{
"typeLabel": "string",
"count": 0
}
]
}
}
}
```
#### 空调控制-相关操作
**POST /app/device/ac/control**
......
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