Commit 5114f34f authored by 张宏's avatar 张宏

空调控制

parent ea90a54a
import 'package:equatable/equatable.dart';
String _parseString(dynamic value, {String fallback = ''}) {
if (value == null) return fallback;
if (value is String) return value;
return value.toString();
}
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;
}
/// 空调状态 BO - 对应 GET /app/device/ac/status 响应
class AcStatusBO extends Equatable {
final int deviceId;
final String deviceName;
final String powerStatus; // 电源开关状态码:0=关闭 1=开启
final String currentTemperature; // 当前室内温度值
final String targetTemperature; // 目标设定温度值
final String mode; // 当前工作模式码:cooling/heating/dehumidify/fan/auto
final String modeText; // 工作模式中文文本
final String fanSpeed; // 当前风速档位码:low/mid/high/auto
final String fanSpeedText; // 风速中文文本
const AcStatusBO({
required this.deviceId,
required this.deviceName,
required this.powerStatus,
required this.currentTemperature,
required this.targetTemperature,
required this.mode,
required this.modeText,
required this.fanSpeed,
required this.fanSpeedText,
});
factory AcStatusBO.fromJson(Map<String, dynamic> json) {
return AcStatusBO(
deviceId: _parseInt(json['deviceId']),
deviceName: _parseString(json['deviceName']),
powerStatus: _parseString(json['powerStatus']),
currentTemperature: _parseString(json['currentTemperature']),
targetTemperature: _parseString(json['targetTemperature']),
mode: _parseString(json['mode']),
modeText: _parseString(json['modeText']),
fanSpeed: _parseString(json['fanSpeed']),
fanSpeedText: _parseString(json['fanSpeedText']),
);
}
/// 电源是否开启
bool get isPowerOn => powerStatus == '1';
/// 当前温度(int)
int get currentTempInt => int.tryParse(currentTemperature) ?? 0;
/// 目标温度(int)
int get targetTempInt => int.tryParse(targetTemperature) ?? 24;
@override
List<Object?> get props => [
deviceId,
deviceName,
powerStatus,
currentTemperature,
targetTemperature,
mode,
modeText,
fanSpeed,
fanSpeedText,
];
}
/// Mode 映射工具
class AcModeMapper {
/// UI 模式名 → GET 返回的 mode 码
static const Map<String, String> uiToGetMode = {
'制冷': 'cooling',
'制热': 'heating',
'除湿': 'dehumidify',
'送风': 'fan',
'自动': 'auto',
};
/// GET 返回的 mode 码 → UI 模式名
static const Map<String, String> getToUiMode = {
'cooling': '制冷',
'heating': '制热',
'dehumidify': '除湿',
'fan': '送风',
'auto': '自动',
};
/// UI 模式名 → POST 请求的 mode 码
static const Map<String, String> uiToPostMode = {
'制冷': 'cool',
'制热': 'heat',
'除湿': 'dry',
'送风': 'fan',
'自动': 'auto',
};
/// UI 风速名 → GET 返回的 API 风速码
static const Map<String, String> uiToGetFanSpeed = {
'自动': 'auto',
'低': 'low',
'中': 'mid',
'高': 'high',
};
/// GET 返回的 API 风速码 → UI 风速名
static const Map<String, String> getToUiFanSpeed = {
'auto': '自动',
'low': '低',
'mid': '中',
'high': '高',
};
/// UI 风速名 → POST 请求的 API 风速码(注意:POST 用 medium,GET 用 mid)
static const Map<String, String> uiToPostFanSpeed = {
'自动': 'auto',
'低': 'low',
'中': 'medium',
'高': 'high',
};
/// 根据 GET mode 码获取 UI 模式名,未知时返回原值
static String modeTextFromCode(String modeCode) {
return getToUiMode[modeCode] ?? modeCode;
}
/// 根据 UI 模式名获取 POST mode 码
static String postModeFromUi(String uiModeName) {
return uiToPostMode[uiModeName] ?? 'cool';
}
/// 根据 GET 风速码获取 UI 风速名
static String fanSpeedTextFromGetCode(String fanSpeedCode) {
return getToUiFanSpeed[fanSpeedCode] ?? fanSpeedCode;
}
/// 根据 UI 风速名获取 POST 风速码
static String postFanSpeedFromUi(String uiFanSpeedName) {
return uiToPostFanSpeed[uiFanSpeedName] ?? 'auto';
}
}
import '../utils/http/response_model.dart';
import '../utils/http/dio_request.dart';
import '../models/bo/ac_bo.dart';
class AcRepository {
/// 获取空调状态
/// GET /app/device/ac/status
Future<ResponseModel<AcStatusBO>> getStatus({required int deviceId}) {
return DioRequest.instance.get<AcStatusBO>(
'/app/device/ac/status',
queryParameters: {'deviceId': deviceId},
fromJsonT: (data) =>
AcStatusBO.fromJson(data as Map<String, dynamic>),
);
}
/// 空调控制操作
/// POST /app/device/ac/control
Future<ResponseModel<Map<String, dynamic>>> control({
required int deviceId,
bool? powerOn,
int? targetTemperature,
String? mode,
String? fanSpeed,
}) {
final params = <String, dynamic>{
'deviceId': deviceId,
};
if (powerOn != null) params['powerOn'] = powerOn;
if (targetTemperature != null) params['targetTemperature'] = targetTemperature;
if (mode != null) params['mode'] = mode;
if (fanSpeed != null) params['fanSpeed'] = fanSpeed;
return DioRequest.instance.post<Map<String, dynamic>>(
'/app/device/ac/control',
data: params,
);
}
}
......@@ -181,9 +181,14 @@ abstract class $AppRouter extends _i21.RootStackRouter {
);
},
ServiceDeviceDetailRoute.name: (routeData) {
final args = routeData.argsAs<ServiceDeviceDetailRouteArgs>(
orElse: () => const ServiceDeviceDetailRouteArgs(deviceId: 0));
return _i21.AutoRoutePage<dynamic>(
routeData: routeData,
child: const _i18.ServiceDeviceDetailView(),
child: _i18.ServiceDeviceDetailView(
key: args.key,
deviceId: args.deviceId,
),
);
},
ServiceIndexRoute.name: (routeData) {
......@@ -586,16 +591,40 @@ class SearchRouteArgs {
/// generated route for
/// [_i18.ServiceDeviceDetailView]
class ServiceDeviceDetailRoute extends _i21.PageRouteInfo<void> {
const ServiceDeviceDetailRoute({List<_i21.PageRouteInfo>? children})
: super(
class ServiceDeviceDetailRoute extends _i21.PageRouteInfo<ServiceDeviceDetailRouteArgs> {
ServiceDeviceDetailRoute({
_i22.Key? key,
required int deviceId,
List<_i21.PageRouteInfo>? children,
}) : super(
ServiceDeviceDetailRoute.name,
args: ServiceDeviceDetailRouteArgs(
key: key,
deviceId: deviceId,
),
initialChildren: children,
);
static const String name = 'ServiceDeviceDetailRoute';
static const _i21.PageInfo<void> page = _i21.PageInfo<void>(name);
static const _i21.PageInfo<ServiceDeviceDetailRouteArgs> page =
_i21.PageInfo<ServiceDeviceDetailRouteArgs>(name);
}
class ServiceDeviceDetailRouteArgs {
const ServiceDeviceDetailRouteArgs({
this.key,
required this.deviceId,
});
final _i22.Key? key;
final int deviceId;
@override
String toString() {
return 'ServiceDeviceDetailRouteArgs{key: $key, deviceId: $deviceId}';
}
}
/// generated route for
......
import '../models/bo/ac_bo.dart';
import '../repositories/ac_repository.dart';
class AcService {
final AcRepository _repository;
AcService({required AcRepository repository}) : _repository = repository;
/// 获取空调状态
Future<AcStatusBO> getStatus({required int deviceId}) async {
final result = await _repository.getStatus(deviceId: deviceId);
if (result.success && result.data != null) {
return result.data!;
}
throw Exception(result.msg);
}
/// 空调控制操作
Future<void> control({
required int deviceId,
bool? powerOn,
int? targetTemperature,
String? mode,
String? fanSpeed,
}) async {
final result = await _repository.control(
deviceId: deviceId,
powerOn: powerOn,
targetTemperature: targetTemperature,
mode: mode,
fanSpeed: fanSpeed,
);
if (!result.success) {
throw Exception(result.msg);
}
}
}
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/ac_bo.dart';
import 'package:smart_hotel_app/repositories/ac_repository.dart';
import 'package:smart_hotel_app/services/ac_service.dart';
import 'package:smart_hotel_app/views/service/device/cubit/service_device_state.dart';
class ServiceDeviceCubit extends Cubit<ServiceDeviceState> {
ServiceDeviceCubit() : super(const ServiceDeviceState()) {
initData();
final AcService _service;
final int _deviceId;
ServiceDeviceCubit({
AcService? service,
required int deviceId,
}) : _service = service ?? AcService(repository: AcRepository()),
_deviceId = deviceId,
super(ServiceDeviceState(deviceId: deviceId)) {
loadAcStatus();
}
/// 加载空调状态
Future<void> loadAcStatus() async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
final status = await _service.getStatus(deviceId: _deviceId);
emit(_mapStatusToState(status));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
void initData() {
emit(state.copyWith(
isOn: true,
targetTemperature: 24,
currentTemperature: 26,
/// 将 API 返回的 AcStatusBO 映射为 State
ServiceDeviceState _mapStatusToState(AcStatusBO status) {
// 构建模式数据列表
const allModes = ['制冷', '制热', '除湿', '送风'];
final currentModeName = AcModeMapper.modeTextFromCode(status.mode);
final modeData = allModes.map((name) {
return {
'name': name,
'icon': _modeIcon(name),
'isSelected': name == currentModeName,
};
}).toList();
// 解析温度
final currentTemp = status.currentTempInt;
final targetTemp = status.targetTempInt;
// 获取当前风速的 UI 名称
final currentSpeed = AcModeMapper.fanSpeedTextFromGetCode(status.fanSpeed);
return ServiceDeviceState(
isLoading: false,
deviceId: _deviceId,
isOn: status.isPowerOn,
currentTemperature: currentTemp > 0 ? currentTemp : 26,
targetTemperature: targetTemp > 0 ? targetTemp : 24,
minTemperature: 16,
maxTemperature: 34,
modeData: [
{'name': '制冷', 'icon': Icons.ac_unit, 'isSelected': true},
{'name': '制热', 'icon': Icons.heat_pump, 'isSelected': false},
{'name': '除湿', 'icon': Icons.water_drop, 'isSelected': false},
{'name': '送风', 'icon': Icons.air, 'isSelected': false},
],
currentSpeed: '自动',
speedOptions: ['自动', '低', '中', '高'],
));
maxTemperature: 30,
modeData: modeData,
currentSpeed: currentSpeed,
speedOptions: const ['自动', '低', '中', '高'],
);
}
/// 获取模式对应的图标
IconData _modeIcon(String modeName) {
switch (modeName) {
case '制冷':
return Icons.ac_unit;
case '制热':
return Icons.heat_pump;
case '除湿':
return Icons.water_drop;
case '送风':
return Icons.air;
default:
return Icons.ac_unit;
}
}
void togglePower(bool value) {
emit(state.copyWith(isOn: value));
/// 开关机
Future<void> togglePower(bool value) async {
if (state.isOperating) return;
// 乐观更新 UI
final previousState = state;
emit(state.copyWith(isOn: value, isOperating: true));
try {
await _service.control(
deviceId: _deviceId,
powerOn: value,
);
emit(state.copyWith(isOperating: false));
} catch (e) {
// 失败时回滚状态
emit(previousState.copyWith(
isOperating: false,
error: e.toString(),
));
}
}
void setTargetTemperature(int temp) {
if (temp >= state.minTemperature && temp <= state.maxTemperature) {
emit(state.copyWith(targetTemperature: temp));
/// 设置目标温度
Future<void> _setTargetTemperature(int temp) async {
if (state.isOperating) return;
if (temp < state.minTemperature || temp > state.maxTemperature) return;
final previousState = state;
emit(state.copyWith(targetTemperature: temp, isOperating: true));
try {
await _service.control(
deviceId: _deviceId,
targetTemperature: temp,
);
emit(state.copyWith(isOperating: false));
} catch (e) {
emit(previousState.copyWith(
isOperating: false,
error: e.toString(),
));
}
}
void increaseTemperature() {
if (state.targetTemperature < state.maxTemperature) {
emit(state.copyWith(targetTemperature: state.targetTemperature + 1));
_setTargetTemperature(state.targetTemperature + 1);
}
}
void decreaseTemperature() {
if (state.targetTemperature > state.minTemperature) {
emit(state.copyWith(targetTemperature: state.targetTemperature - 1));
_setTargetTemperature(state.targetTemperature - 1);
}
}
void selectMode(int index) {
/// 选择模式
Future<void> selectMode(int index) async {
if (state.isOperating) return;
if (index < 0 || index >= state.modeData.length) return;
final previousState = state;
final updatedModeData = state.modeData.map((mode) {
return {...mode, 'isSelected': false};
}).toList();
updatedModeData[index]['isSelected'] = true;
emit(state.copyWith(modeData: updatedModeData));
final modeName = updatedModeData[index]['name'] as String;
emit(state.copyWith(modeData: updatedModeData, isOperating: true));
try {
await _service.control(
deviceId: _deviceId,
mode: AcModeMapper.postModeFromUi(modeName),
);
emit(state.copyWith(isOperating: false));
} catch (e) {
emit(previousState.copyWith(
isOperating: false,
error: e.toString(),
));
}
}
void setWindSpeed(String speed) {
emit(state.copyWith(currentSpeed: speed));
/// 设置风速
Future<void> setWindSpeed(String speed) async {
if (state.isOperating) return;
final previousState = state;
emit(state.copyWith(currentSpeed: speed, isOperating: true));
try {
await _service.control(
deviceId: _deviceId,
fanSpeed: AcModeMapper.postFanSpeedFromUi(speed),
);
emit(state.copyWith(isOperating: false));
} catch (e) {
emit(previousState.copyWith(
isOperating: false,
error: e.toString(),
));
}
}
}
import 'package:equatable/equatable.dart';
class ServiceDeviceState extends Equatable {
final bool isLoading;
final String? error;
final bool isOperating; // 操作进行中(防重复提交)
final int deviceId;
final bool isOn;
final int targetTemperature;
final int currentTemperature;
......@@ -11,17 +15,25 @@ class ServiceDeviceState extends Equatable {
final List<String> speedOptions;
const ServiceDeviceState({
this.isLoading = false,
this.error,
this.isOperating = false,
this.deviceId = 0,
this.isOn = true,
this.targetTemperature = 24,
this.currentTemperature = 26,
this.minTemperature = 16,
this.maxTemperature = 34,
this.maxTemperature = 30,
this.modeData = const [],
this.currentSpeed = '自动',
this.speedOptions = const ['自动', '低', '中', '高'],
});
ServiceDeviceState copyWith({
bool? isLoading,
String? error,
bool? isOperating,
int? deviceId,
bool? isOn,
int? targetTemperature,
int? currentTemperature,
......@@ -30,8 +42,13 @@ class ServiceDeviceState extends Equatable {
List<Map<String, dynamic>>? modeData,
String? currentSpeed,
List<String>? speedOptions,
bool clearError = false,
}) {
return ServiceDeviceState(
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
isOperating: isOperating ?? this.isOperating,
deviceId: deviceId ?? this.deviceId,
isOn: isOn ?? this.isOn,
targetTemperature: targetTemperature ?? this.targetTemperature,
currentTemperature: currentTemperature ?? this.currentTemperature,
......@@ -45,6 +62,10 @@ class ServiceDeviceState extends Equatable {
@override
List<Object?> get props => [
isLoading,
error,
isOperating,
deviceId,
isOn,
targetTemperature,
currentTemperature,
......
......@@ -10,12 +10,14 @@ import 'package:smart_hotel_app/views/service/device/widget/wind_speed_block.dar
@RoutePage()
class ServiceDeviceDetailView extends StatelessWidget {
const ServiceDeviceDetailView({super.key});
final int deviceId;
const ServiceDeviceDetailView({super.key, required this.deviceId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ServiceDeviceCubit(),
create: (_) => ServiceDeviceCubit(deviceId: deviceId),
child: Scaffold(
backgroundColor: const Color.fromRGBO(242, 243, 245, 1),
appBar: AppBar(
......@@ -38,12 +40,36 @@ class ServiceDeviceDetailView extends StatelessWidget {
),
centerTitle: true,
),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: BlocBuilder<ServiceDeviceCubit, ServiceDeviceState>(
body: BlocBuilder<ServiceDeviceCubit, ServiceDeviceState>(
builder: (context, state) {
final cubit = context.read<ServiceDeviceCubit>();
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: () => cubit.loadAcStatus(),
child: const Text('重试'),
),
],
),
);
}
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: Column(
children: [
SizedBox(height: 20.h),
TemperatureControlBlock(
......@@ -69,11 +95,11 @@ class ServiceDeviceDetailView extends StatelessWidget {
),
SizedBox(height: 20.h),
],
),
);
},
),
),
),
);
}
}
......@@ -49,10 +49,12 @@ class ServiceRoomCubit extends Cubit<ServiceRoomState> {
final controlDevices = detail.deviceControl.subDeviceControls.map((c) {
return ControlDevice(
deviceId: c.deviceId,
name: c.deviceName,
icon: _mapDeviceIcon(c.deviceTypeName, ''),
isOn: c.powerOn,
detailText: c.currentParamDisplay.isNotEmpty ? c.currentParamDisplay : null,
deviceTypeName: c.deviceTypeName,
);
}).toList();
......@@ -192,10 +194,12 @@ class ServiceRoomCubit extends Cubit<ServiceRoomState> {
// navigates to ServiceDeviceDetailRoute for AC control.
final updatedDevices = List<ControlDevice>.from(devices);
updatedDevices[index] = ControlDevice(
deviceId: updatedDevices[index].deviceId,
name: updatedDevices[index].name,
icon: updatedDevices[index].icon,
isOn: value,
detailText: updatedDevices[index].detailText,
deviceTypeName: updatedDevices[index].deviceTypeName,
);
emit(state.copyWith(controlDevices: updatedDevices));
}
......
......@@ -18,16 +18,20 @@ class RoomDevice {
}
class ControlDevice {
final int deviceId;
final String name;
final IconData icon;
final bool isOn;
final String? detailText;
final String? deviceTypeName;
const ControlDevice({
this.deviceId = 0,
required this.name,
required this.icon,
this.isOn = false,
this.detailText,
this.deviceTypeName,
});
}
......
......@@ -122,8 +122,8 @@ class ServiceRoomDetailView extends StatelessWidget {
onDeviceToggle: (index, value) =>
cubit.toggleControlDevice(index, value),
onDeviceTap: (index, device) {
if (device.name == '空调') {
context.pushRoute(const ServiceDeviceDetailRoute());
if (device.deviceTypeName == '空调' || device.deviceTypeName?.contains('空调') == true) {
context.pushRoute(ServiceDeviceDetailRoute(deviceId: device.deviceId));
}
},
),
......
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