Files
petwash/PetWashControl/ViewModels/MainViewModel.cs
GukSang.Jin 0a884fa6cb 更新
2026-03-18 13:53:44 +08:00

1747 lines
62 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PetWashControl.Models;
using PetWashControl.Services;
using QRCoder;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace PetWashControl.ViewModels;
public partial class WashStep : ObservableObject
{
[ObservableProperty]
private string _name = "";
[ObservableProperty]
private string _status = "等待中";
[ObservableProperty]
private bool _isActive;
}
public sealed record PackageTimingProfile(
int FirstSprayWaterTime,
int AfterShampoo1SprayTime,
int AfterShampoo2SprayTime,
int AfterShampoo3SprayTime,
int SprayShampoo1Time,
int SprayShampoo2Time,
int SprayShampoo3Time,
int HotAirTime,
int ColdAirTime,
int UvSterilizationTime);
public partial class MainViewModel : ObservableObject
{
private readonly ApiService _apiService;
private readonly MqttClientService _mqttService;
private readonly ModbusService _modbusService;
private readonly ConfigurationService _config;
private readonly LogService _logger;
public event Action<string>? ViewChanged;
public event Action? OpenPackageManagementRequested;
[ObservableProperty]
private ObservableCollection<Package> _packages = new();
[ObservableProperty]
private Package? _selectedPackage;
[ObservableProperty]
private Order? _currentOrder;
[ObservableProperty]
private ImageSource? _paymentQrCodeImage;
[ObservableProperty]
private string _paymentCodeUrl = "";
[ObservableProperty]
private string _paymentOutTradeNo = "";
[ObservableProperty]
private DateTimeOffset? _paymentExpiresAt;
[ObservableProperty]
private string _paymentCountdownText = "";
[ObservableProperty]
private string _paymentStatusText = "等待扫码支付";
[ObservableProperty]
private bool _isPaymentExpired;
[ObservableProperty]
private string _statusMessage = "系统就绪";
[ObservableProperty]
private bool _isDoorOpen;
[ObservableProperty]
private bool _isWashing;
[ObservableProperty]
private bool _isConnected;
[ObservableProperty]
private bool _isModbusConnected;
[ObservableProperty]
private string _currentView = "Idle";
private string _previousView = "Idle"; // 保存进入设置前的视图
[ObservableProperty]
private int _washProgress;
[ObservableProperty]
private string _currentStep = "";
[ObservableProperty]
private string _remainingTime = "00:00";
[ObservableProperty]
private ObservableCollection<WashStep> _washSteps = new();
[ObservableProperty]
private double _waterTemperature = 40.0;
[ObservableProperty]
private double _roomTemperature = 25.0;
[ObservableProperty]
private string _currentCarouselImage = "/Images/dog.png";
[ObservableProperty]
private string _deviceNumber = "设备编号: CSI-PW-001";
[ObservableProperty]
private string _currentDateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
[ObservableProperty]
private string _currentDayOfWeek = DateTime.Now.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
[ObservableProperty]
private bool _isInstructionDialogOpen;
[ObservableProperty]
private bool _isCustomerServiceDialogOpen;
[ObservableProperty]
private Package? _editingPackage;
[ObservableProperty]
private decimal _editingPackagePrice = 0.01m;
[ObservableProperty]
private int _editingPackageFirstSprayWaterTime = 2;
[ObservableProperty]
private int _editingPackageAfterShampoo1SprayTime = 2;
[ObservableProperty]
private int _editingPackageAfterShampoo2SprayTime = 2;
[ObservableProperty]
private int _editingPackageAfterShampoo3SprayTime = 2;
[ObservableProperty]
private int _editingPackageSprayShampoo1Time = 1;
[ObservableProperty]
private int _editingPackageSprayShampoo2Time = 1;
[ObservableProperty]
private int _editingPackageSprayShampoo3Time = 1;
[ObservableProperty]
private int _editingPackageColdAirTime = 2;
[ObservableProperty]
private int _editingPackageHotAirTime = 5;
[ObservableProperty]
private int _editingPackageUvSterilizationTime = 2;
[ObservableProperty]
private int _firstSprayWaterTime = 2;
[ObservableProperty]
private int _afterShampoo1SprayTime = 2;
[ObservableProperty]
private int _afterShampoo2SprayTime = 2;
[ObservableProperty]
private int _afterShampoo3SprayTime = 2;
[ObservableProperty]
private int _sprayShampoo1Time = 1;
[ObservableProperty]
private int _sprayShampoo2Time = 1;
[ObservableProperty]
private int _sprayShampoo3Time = 1;
[ObservableProperty]
private int _coldAirTime = 2;
[ObservableProperty]
private int _hotAirTime = 5;
[ObservableProperty]
private int _uvSterilizationTime = 3;
[ObservableProperty]
private bool _isCleaningDialogOpen;
[ObservableProperty]
private string _cleaningMessage = "正在清理笼子...";
[ObservableProperty]
private int _cleaningProgress;
[ObservableProperty]
private int _shampoo1Level = 80;
[ObservableProperty]
private int _shampoo2Level = 75;
[ObservableProperty]
private int _shampoo3Level = 70;
private readonly System.Timers.Timer _carouselTimer;
private readonly System.Timers.Timer _clockTimer;
private readonly System.Timers.Timer _liquidLevelTimer;
private readonly DispatcherTimer _paymentStatusTimer;
private readonly DispatcherTimer _paymentCountdownTimer;
private readonly string[] _carouselImages = { "/Images/dog.png", "/Images/dog1.png", "/Images/dog2.png" };
private int _currentImageIndex = 0;
private bool _isPaymentPolling;
private bool _isCheckingPaymentStatus;
public int EditingPackageTotalDuration =>
EditingPackageFirstSprayWaterTime +
EditingPackageAfterShampoo1SprayTime +
EditingPackageAfterShampoo2SprayTime +
EditingPackageAfterShampoo3SprayTime +
EditingPackageSprayShampoo1Time +
EditingPackageSprayShampoo2Time +
EditingPackageSprayShampoo3Time +
EditingPackageHotAirTime +
EditingPackageColdAirTime +
EditingPackageUvSterilizationTime;
public MainViewModel()
{
_config = new ConfigurationService();
_logger = new LogService();
_apiService = new ApiService(_config);
_mqttService = new MqttClientService(_config);
_modbusService = new ModbusService(_config, _logger);
_mqttService.MessageReceived += OnMqttMessageReceived;
_modbusService.ConnectionStatusChanged += OnModbusConnectionStatusChanged;
// 从配置加载时间参数
FirstSprayWaterTime = _config.FirstSprayWaterTime;
AfterShampoo1SprayTime = _config.AfterShampoo1SprayTime;
AfterShampoo2SprayTime = _config.AfterShampoo2SprayTime;
AfterShampoo3SprayTime = _config.AfterShampoo3SprayTime;
SprayShampoo1Time = _config.SprayShampoo1Time;
SprayShampoo2Time = _config.SprayShampoo2Time;
SprayShampoo3Time = _config.SprayShampoo3Time;
ColdAirTime = _config.ColdAirTime;
HotAirTime = _config.HotAirTime;
UvSterilizationTime = _config.UvSterilizationTime;
// 初始化洗护步骤
InitializeWashSteps();
// 初始化图片轮播定时器每5秒切换
_carouselTimer = new System.Timers.Timer(5000);
_carouselTimer.Elapsed += (s, e) =>
{
Application.Current.Dispatcher.Invoke(() =>
{
_currentImageIndex = (_currentImageIndex + 1) % _carouselImages.Length;
CurrentCarouselImage = _carouselImages[_currentImageIndex];
});
};
_carouselTimer.Start();
// 初始化时钟定时器(每秒更新)
_clockTimer = new System.Timers.Timer(1000);
_clockTimer.Elapsed += (s, e) =>
{
Application.Current.Dispatcher.Invoke(() =>
{
var now = DateTime.Now;
CurrentDateTime = now.ToString("yyyy-MM-dd HH:mm:ss");
CurrentDayOfWeek = now.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
});
};
_clockTimer.Start();
// 初始化液位监测定时器每30秒更新一次
_liquidLevelTimer = new System.Timers.Timer(30000);
_liquidLevelTimer.Elapsed += async (s, e) =>
{
if (IsModbusConnected)
{
try
{
await LoadLiquidLevelsAsync();
}
catch (Exception ex)
{
_logger.LogError("定时更新液位失败", ex);
}
}
};
_liquidLevelTimer.Start();
_paymentStatusTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(Math.Max(1, _config.PaymentCheckIntervalSeconds))
};
_paymentStatusTimer.Tick += async (s, e) => await PollPaymentStatusAsync();
_paymentCountdownTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_paymentCountdownTimer.Tick += async (s, e) => await RefreshPaymentCountdownAsync();
}
private void InitializeWashSteps()
{
WashSteps.Add(new WashStep { Name = "第一次冲水", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "沐浴露喷洒", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "第二次冲水", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "香波喷洒", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "第三次冲水", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "护理液喷洒", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "第四次冲水", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "热风吹毛", Status = "等待中" });
WashSteps.Add(new WashStep { Name = "冷热风混合", Status = "等待中" });
// 紫外线杀菌不在洗护流程中,在流程完成后独立执行
}
public async Task InitializeAsync()
{
try
{
_logger.LogInfo("开始初始化系统...");
// 连接 Modbus TCP 设备
_logger.LogInfo($"正在连接 Modbus TCP 设备: {_config.ModbusIpAddress}:{_config.ModbusPort}");
var modbusConnected = await _modbusService.ConnectAsync();
IsModbusConnected = modbusConnected;
if (modbusConnected)
{
_logger.LogInfo("Modbus TCP 设备连接成功");
// 从设备读取初始配置参数
await LoadDeviceParametersAsync();
}
else
{
_logger.LogWarning("Modbus TCP 设备连接失败,使用默认配置");
LoadDefaultParameters();
}
// 连接 MQTT
await _mqttService.ConnectAsync();
IsConnected = _mqttService.IsConnected;
_logger.LogInfo("MQTT连接成功");
await LoadPackagesAsync();
if (IsModbusConnected)
{
StatusMessage = "系统就绪,设备已连接";
}
else
{
StatusMessage = "系统就绪,设备未连接";
}
_logger.LogInfo("系统初始化完成");
}
catch (Exception ex)
{
_logger.LogError("初始化失败", ex);
StatusMessage = $"初始化失败: {ex.Message}";
MessageBox.Show($"系统初始化失败,请检查后端服务和设备连接。\n\n错误: {ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 从 Modbus 设备读取系统参数
/// </summary>
private async Task LoadDeviceParametersAsync()
{
try
{
_logger.LogInfo("正在从设备读取系统参数...");
// 读取时间参数D404-D424共22个寄存器11个浮点数
// 每个浮点数占用2个寄存器
var timeParams = await _modbusService.ReadHoldingRegistersAsync(404, 22);
// 使用 ConvertRegistersToFloat 转换每个参数CDAB字节序
FirstSprayWaterTime = (int)_modbusService.ConvertRegistersToFloat(timeParams, 0, FloatByteOrder.CDAB); // D404-D405
AfterShampoo1SprayTime = (int)_modbusService.ConvertRegistersToFloat(timeParams, 2, FloatByteOrder.CDAB); // D406-D407
AfterShampoo2SprayTime = (int)_modbusService.ConvertRegistersToFloat(timeParams, 4, FloatByteOrder.CDAB); // D408-D409
AfterShampoo3SprayTime = (int)_modbusService.ConvertRegistersToFloat(timeParams, 6, FloatByteOrder.CDAB); // D410-D411
// D412-D413 清洗笼子喷水保留索引8
SprayShampoo1Time = (int)_modbusService.ConvertRegistersToFloat(timeParams, 10, FloatByteOrder.CDAB); // D414-D415
SprayShampoo2Time = (int)_modbusService.ConvertRegistersToFloat(timeParams, 12, FloatByteOrder.CDAB); // D416-D417
SprayShampoo3Time = (int)_modbusService.ConvertRegistersToFloat(timeParams, 14, FloatByteOrder.CDAB); // D418-D419
HotAirTime = (int)_modbusService.ConvertRegistersToFloat(timeParams, 16, FloatByteOrder.CDAB); // D420-D421
ColdAirTime = (int)_modbusService.ConvertRegistersToFloat(timeParams, 18, FloatByteOrder.CDAB); // D422-D423
UvSterilizationTime = (int)_modbusService.ConvertRegistersToFloat(timeParams, 20, FloatByteOrder.CDAB); // D424-D425
_logger.LogInfo($"时间参数读取成功 - 首次喷水:{FirstSprayWaterTime}min, 沐浴1后:{AfterShampoo1SprayTime}min, " +
$"沐浴2后:{AfterShampoo2SprayTime}min, 沐浴3后:{AfterShampoo3SprayTime}min, " +
$"喷沐浴露1:{SprayShampoo1Time}min, 喷沐浴露2:{SprayShampoo2Time}min, 喷沐浴露3:{SprayShampoo3Time}min, " +
$"热风:{HotAirTime}min, 冷风:{ColdAirTime}min, 紫外线:{UvSterilizationTime}min");
// 读取液位参数
await LoadLiquidLevelsAsync();
StatusMessage = "设备参数加载成功";
}
catch (Exception ex)
{
_logger.LogError("读取设备参数失败,使用默认配置", ex);
LoadDefaultParameters();
StatusMessage = "设备参数读取失败,使用默认配置";
}
}
/// <summary>
/// 读取液位传感器数据
/// </summary>
private async Task LoadLiquidLevelsAsync()
{
try
{
// 读取1号液位 (D1280)
var level1 = await _modbusService.ReadHoldingRegistersAsync(1280, 1);
Shampoo1Level = level1[0];
// 读取2号液位 (D1330)
var level2 = await _modbusService.ReadHoldingRegistersAsync(1330, 1);
Shampoo2Level = level2[0];
// 读取3号液位 (D1380)
var level3 = await _modbusService.ReadHoldingRegistersAsync(1380, 1);
Shampoo3Level = level3[0];
_logger.LogInfo($"液位读取成功 - 1号:{Shampoo1Level}%, 2号:{Shampoo2Level}%, 3号:{Shampoo3Level}%");
}
catch (Exception ex)
{
_logger.LogError("读取液位失败", ex);
}
}
/// <summary>
/// 加载默认参数
/// </summary>
private void LoadDefaultParameters()
{
FirstSprayWaterTime = _config.FirstSprayWaterTime;
AfterShampoo1SprayTime = _config.AfterShampoo1SprayTime;
AfterShampoo2SprayTime = _config.AfterShampoo2SprayTime;
AfterShampoo3SprayTime = _config.AfterShampoo3SprayTime;
SprayShampoo1Time = _config.SprayShampoo1Time;
SprayShampoo2Time = _config.SprayShampoo2Time;
SprayShampoo3Time = _config.SprayShampoo3Time;
ColdAirTime = _config.ColdAirTime;
HotAirTime = _config.HotAirTime;
UvSterilizationTime = _config.UvSterilizationTime;
_logger.LogInfo("已加载默认参数配置");
}
[RelayCommand]
private async Task LoadPackagesAsync()
{
try
{
_logger.LogInfo("加载套餐列表...");
var packages = await _apiService.GetPackagesAsync();
Packages.Clear();
foreach (var package in packages)
{
Packages.Add(package);
}
_logger.LogInfo($"成功加载 {packages.Count} 个套餐");
}
catch (Exception ex)
{
_logger.LogError("加载套餐失败", ex);
StatusMessage = $"加载套餐失败: {ex.Message}";
}
}
[RelayCommand]
private void ShowPayment()
{
CurrentView = "Payment";
ViewChanged?.Invoke("Payment");
StatusMessage = "请选择套餐";
_logger.LogInfo("切换到支付界面");
}
[RelayCommand]
private void ShowSettings()
{
// 保存当前视图状态
_previousView = CurrentView;
CurrentView = "Settings";
ViewChanged?.Invoke("Settings");
StatusMessage = "系统设置";
_logger.LogInfo($"切换到设置界面,之前的视图: {_previousView}");
}
public void ShowSettingsFromAdmin()
{
ShowSettings();
}
[RelayCommand]
private void ShowInstruction()
{
IsInstructionDialogOpen = true;
_logger.LogInfo("显示使用说明");
}
[RelayCommand]
private void CloseInstruction()
{
IsInstructionDialogOpen = false;
_logger.LogInfo("关闭使用说明");
}
[RelayCommand]
private void ShowCustomerService()
{
IsCustomerServiceDialogOpen = true;
_logger.LogInfo("显示联系客服");
}
[RelayCommand]
private void CloseCustomerService()
{
IsCustomerServiceDialogOpen = false;
_logger.LogInfo("关闭联系客服");
}
[RelayCommand]
private async Task SaveSettingsAsync()
{
try
{
StatusMessage = "正在保存参数...";
// 更新本地配置
_config.FirstSprayWaterTime = FirstSprayWaterTime;
_config.AfterShampoo1SprayTime = AfterShampoo1SprayTime;
_config.AfterShampoo2SprayTime = AfterShampoo2SprayTime;
_config.AfterShampoo3SprayTime = AfterShampoo3SprayTime;
_config.SprayShampoo1Time = SprayShampoo1Time;
_config.SprayShampoo2Time = SprayShampoo2Time;
_config.SprayShampoo3Time = SprayShampoo3Time;
_config.ColdAirTime = ColdAirTime;
_config.HotAirTime = HotAirTime;
_config.UvSterilizationTime = UvSterilizationTime;
_config.Save();
_logger.LogInfo($"本地参数已更新 - 首次喷水:{FirstSprayWaterTime}min, 沐浴1后:{AfterShampoo1SprayTime}min, " +
$"沐浴2后:{AfterShampoo2SprayTime}min, 沐浴3后:{AfterShampoo3SprayTime}min, " +
$"喷沐浴露1:{SprayShampoo1Time}min, 喷沐浴露2:{SprayShampoo2Time}min, 喷沐浴露3:{SprayShampoo3Time}min, " +
$"冷风:{ColdAirTime}min, 热风:{HotAirTime}min, 紫外线:{UvSterilizationTime}min");
// 如果 Modbus 已连接,写入设备
if (IsModbusConnected)
{
await SaveParametersToDeviceAsync();
MessageBox.Show("系统参数已保存到设备!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
StatusMessage = "参数保存成功";
}
else
{
MessageBox.Show("系统参数已保存到本地配置!\n\n注意设备未连接参数未写入设备。",
"保存成功", MessageBoxButton.OK, MessageBoxImage.Warning);
StatusMessage = "参数已保存(仅本地)";
}
}
catch (Exception ex)
{
_logger.LogError("保存系统参数失败", ex);
StatusMessage = "参数保存失败";
MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 将参数保存到 Modbus 设备
/// </summary>
private async Task SaveParametersToDeviceAsync()
{
try
{
_logger.LogInfo("正在将参数写入设备...");
// 准备时间参数数据D404-D425共22个寄存器11个浮点数
// 每个浮点数占用2个寄存器
List<ushort> allRegisters = new List<ushort>();
// 使用 ConvertFloatToRegisters 转换每个参数CDAB字节序
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(FirstSprayWaterTime, FloatByteOrder.CDAB)); // D404-D405
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(AfterShampoo1SprayTime, FloatByteOrder.CDAB)); // D406-D407
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(AfterShampoo2SprayTime, FloatByteOrder.CDAB)); // D408-D409
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(AfterShampoo3SprayTime, FloatByteOrder.CDAB)); // D410-D411
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(0, FloatByteOrder.CDAB)); // D412-D413 清洗笼子喷水(保留)
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(SprayShampoo1Time, FloatByteOrder.CDAB)); // D414-D415
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(SprayShampoo2Time, FloatByteOrder.CDAB)); // D416-D417
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(SprayShampoo3Time, FloatByteOrder.CDAB)); // D418-D419
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(HotAirTime, FloatByteOrder.CDAB)); // D420-D421
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(ColdAirTime, FloatByteOrder.CDAB)); // D422-D423
allRegisters.AddRange(_modbusService.ConvertFloatToRegisters(UvSterilizationTime, FloatByteOrder.CDAB)); // D424-D425
// 写入设备22个寄存器
await _modbusService.WriteMultipleRegistersAsync(404, allRegisters.ToArray());
_logger.LogInfo("参数已成功写入设备");
}
catch (Exception ex)
{
_logger.LogError("写入设备参数失败", ex);
throw new Exception($"写入设备失败: {ex.Message}", ex);
}
}
private PackageTimingProfile GetPackageTimingProfile(Package package)
{
if (package.FirstSprayWaterTime > 0 &&
package.AfterShampoo1SprayTime > 0 &&
package.AfterShampoo2SprayTime > 0 &&
package.AfterShampoo3SprayTime > 0 &&
package.SprayShampoo1Time > 0 &&
package.SprayShampoo2Time > 0 &&
package.SprayShampoo3Time > 0 &&
package.HotAirTime > 0 &&
package.ColdAirTime > 0 &&
package.UvSterilizationTime > 0)
{
return new PackageTimingProfile(
package.FirstSprayWaterTime,
package.AfterShampoo1SprayTime,
package.AfterShampoo2SprayTime,
package.AfterShampoo3SprayTime,
package.SprayShampoo1Time,
package.SprayShampoo2Time,
package.SprayShampoo3Time,
package.HotAirTime,
package.ColdAirTime,
package.UvSterilizationTime);
}
return new PackageTimingProfile(
_config.FirstSprayWaterTime,
_config.AfterShampoo1SprayTime,
_config.AfterShampoo2SprayTime,
_config.AfterShampoo3SprayTime,
_config.SprayShampoo1Time,
_config.SprayShampoo2Time,
_config.SprayShampoo3Time,
_config.HotAirTime,
_config.ColdAirTime,
_config.UvSterilizationTime);
}
private void ApplyPackageTimingProfile(PackageTimingProfile profile)
{
FirstSprayWaterTime = profile.FirstSprayWaterTime;
AfterShampoo1SprayTime = profile.AfterShampoo1SprayTime;
AfterShampoo2SprayTime = profile.AfterShampoo2SprayTime;
AfterShampoo3SprayTime = profile.AfterShampoo3SprayTime;
SprayShampoo1Time = profile.SprayShampoo1Time;
SprayShampoo2Time = profile.SprayShampoo2Time;
SprayShampoo3Time = profile.SprayShampoo3Time;
HotAirTime = profile.HotAirTime;
ColdAirTime = profile.ColdAirTime;
UvSterilizationTime = profile.UvSterilizationTime;
}
private async Task ApplyPackageTimingProfileAsync(Package package)
{
var profile = GetPackageTimingProfile(package);
ApplyPackageTimingProfile(profile);
_logger.LogInfo(
$"套餐参数已切换: PackageId={package.Id}, FirstSpray={FirstSprayWaterTime}, Shampoo1={SprayShampoo1Time}, " +
$"Rinse1={AfterShampoo1SprayTime}, Shampoo2={SprayShampoo2Time}, Rinse2={AfterShampoo2SprayTime}, " +
$"Conditioner={SprayShampoo3Time}, Rinse3={AfterShampoo3SprayTime}, HotAir={HotAirTime}, ColdAir={ColdAirTime}, UV={UvSterilizationTime}");
if (IsModbusConnected)
{
await SaveParametersToDeviceAsync();
}
}
// 参数调整命令
[RelayCommand]
private void IncreaseFirstSprayWater() => FirstSprayWaterTime = Math.Min(FirstSprayWaterTime + 1, 60);
[RelayCommand]
private void DecreaseFirstSprayWater() => FirstSprayWaterTime = Math.Max(FirstSprayWaterTime - 1, 0);
[RelayCommand]
private void IncreaseSprayShampoo1() => SprayShampoo1Time = Math.Min(SprayShampoo1Time + 1, 60);
[RelayCommand]
private void DecreaseSprayShampoo1() => SprayShampoo1Time = Math.Max(SprayShampoo1Time - 1, 0);
[RelayCommand]
private void IncreaseAfterShampoo1Spray() => AfterShampoo1SprayTime = Math.Min(AfterShampoo1SprayTime + 1, 60);
[RelayCommand]
private void DecreaseAfterShampoo1Spray() => AfterShampoo1SprayTime = Math.Max(AfterShampoo1SprayTime - 1, 0);
[RelayCommand]
private void IncreaseSprayShampoo2() => SprayShampoo2Time = Math.Min(SprayShampoo2Time + 1, 60);
[RelayCommand]
private void DecreaseSprayShampoo2() => SprayShampoo2Time = Math.Max(SprayShampoo2Time - 1, 0);
[RelayCommand]
private void IncreaseAfterShampoo2Spray() => AfterShampoo2SprayTime = Math.Min(AfterShampoo2SprayTime + 1, 60);
[RelayCommand]
private void DecreaseAfterShampoo2Spray() => AfterShampoo2SprayTime = Math.Max(AfterShampoo2SprayTime - 1, 0);
[RelayCommand]
private void IncreaseSprayShampoo3() => SprayShampoo3Time = Math.Min(SprayShampoo3Time + 1, 60);
[RelayCommand]
private void DecreaseSprayShampoo3() => SprayShampoo3Time = Math.Max(SprayShampoo3Time - 1, 0);
[RelayCommand]
private void IncreaseAfterShampoo3Spray() => AfterShampoo3SprayTime = Math.Min(AfterShampoo3SprayTime + 1, 60);
[RelayCommand]
private void DecreaseAfterShampoo3Spray() => AfterShampoo3SprayTime = Math.Max(AfterShampoo3SprayTime - 1, 0);
[RelayCommand]
private void IncreaseColdAir() => ColdAirTime = Math.Min(ColdAirTime + 1, 60);
[RelayCommand]
private void DecreaseColdAir() => ColdAirTime = Math.Max(ColdAirTime - 1, 0);
[RelayCommand]
private void IncreaseHotAir() => HotAirTime = Math.Min(HotAirTime + 1, 60);
[RelayCommand]
private void DecreaseHotAir() => HotAirTime = Math.Max(HotAirTime - 1, 0);
[RelayCommand]
private void IncreaseUvSterilization() => UvSterilizationTime = Math.Min(UvSterilizationTime + 1, 60);
[RelayCommand]
private void DecreaseUvSterilization() => UvSterilizationTime = Math.Max(UvSterilizationTime - 1, 0);
[RelayCommand]
private void BackToIdle()
{
// 如果从设置页面返回,且之前不是待机界面,则返回到之前的视图
if (CurrentView == "Settings" && _previousView != "Idle" && _previousView != "Settings")
{
CurrentView = _previousView;
ViewChanged?.Invoke(_previousView);
// 根据返回的视图设置相应的状态消息
switch (_previousView)
{
case "Washing":
StatusMessage = "洗护进行中...";
_logger.LogInfo("从设置返回到洗护界面");
break;
case "Payment":
StatusMessage = "请选择套餐";
_logger.LogInfo("从设置返回到支付界面");
break;
case "QRCode":
StatusMessage = $"请扫码支付 ¥{SelectedPackage?.Price}";
_logger.LogInfo("从设置返回到二维码支付界面");
break;
default:
StatusMessage = "系统就绪,请点击开始";
_logger.LogInfo("从设置返回到待机界面");
break;
}
// 重置之前的视图状态
_previousView = "Idle";
return;
}
// 默认返回到待机界面
CurrentView = "Idle";
ViewChanged?.Invoke("Idle");
StatusMessage = "系统就绪,请点击开始";
SelectedPackage = null;
CurrentOrder = null;
IsDoorOpen = false;
// 重置洗护步骤状态
foreach (var step in WashSteps)
{
step.Status = "等待中";
step.IsActive = false;
}
_previousView = "Idle";
_logger.LogInfo("返回待机界面");
}
[RelayCommand]
private async Task SelectPackage(Package package)
{
if (package == null) return;
SelectedPackage = package;
_logger.LogInfo($"选择套餐: {package.Name}");
try
{
await CreatePaymentOrderAsync(package, true);
}
catch (Exception ex)
{
StopPaymentStatusPolling();
_logger.LogError("创建订单失败", ex);
StatusMessage = $"创建订单失败: {ex.Message}";
MessageBox.Show($"创建订单失败\n\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
[RelayCommand]
private async Task CancelPayment()
{
_logger.LogInfo("取消支付");
await CancelCurrentPaymentAsync("已取消支付,请重新选择套餐");
}
[RelayCommand]
private async Task CreateOrderAsync()
{
if (SelectedPackage == null)
{
MessageBox.Show("请先选择套餐", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
await CreatePaymentOrderAsync(SelectedPackage, false);
}
catch (Exception ex)
{
StopPaymentStatusPolling();
_logger.LogError("创建订单失败", ex);
StatusMessage = $"创建订单失败: {ex.Message}";
MessageBox.Show($"创建订单失败\n\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
[RelayCommand]
private async Task SimulatePaymentAsync()
{
if (CurrentOrder == null)
{
MessageBox.Show("请先创建订单", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
_logger.LogInfo($"模拟支付订单ID: {CurrentOrder.Id}");
StopPaymentStatusPolling();
// 模拟支付处理延迟
StatusMessage = "正在处理支付...";
await Task.Delay(1500);
// 确认支付
CurrentOrder = await _apiService.ConfirmPaymentAsync(CurrentOrder.Id);
await HandlePaymentSuccessAsync();
}
catch (Exception ex)
{
_logger.LogError("支付失败", ex);
StatusMessage = $"支付失败: {ex.Message}";
MessageBox.Show($"支付失败\n\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
[RelayCommand]
private async Task RegeneratePaymentQrAsync()
{
if (SelectedPackage == null)
{
return;
}
try
{
StopPaymentStatusPolling();
PaymentStatusText = "正在刷新二维码...";
StatusMessage = "正在刷新二维码...";
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Cancelled);
}
await CreatePaymentOrderAsync(SelectedPackage, true);
}
catch (Exception ex)
{
_logger.LogError("刷新支付二维码失败", ex);
PaymentStatusText = "二维码刷新失败";
StatusMessage = $"二维码刷新失败: {ex.Message}";
MessageBox.Show($"二维码刷新失败\n\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task CreatePaymentOrderAsync(Package package, bool navigateToQrCode)
{
_logger.LogInfo($"创建订单套餐ID: {package.Id}");
await ApplyPackageTimingProfileAsync(package);
var createOrderResponse = await _apiService.CreateOrderAsync(package.Id);
CurrentOrder = createOrderResponse?.Order;
PaymentCodeUrl = createOrderResponse?.CodeUrl ?? "";
PaymentOutTradeNo = createOrderResponse?.OutTradeNo ?? "";
PaymentExpiresAt = createOrderResponse?.ExpiresAt;
IsPaymentExpired = false;
PaymentStatusText = "等待扫码支付";
RefreshPaymentCountdown();
PaymentQrCodeImage = string.IsNullOrWhiteSpace(PaymentCodeUrl)
? null
: BuildPaymentQrCodeImage(PaymentCodeUrl);
if (CurrentOrder == null || PaymentQrCodeImage == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
throw new InvalidOperationException("Payment QR code was not returned by the server.");
}
StatusMessage = $"订单创建成功,请扫码支付 ¥{package.DisplayPrice}";
_logger.LogInfo($"订单创建成功订单ID: {CurrentOrder?.Id}");
if (navigateToQrCode)
{
CurrentView = "QRCode";
ViewChanged?.Invoke("QRCode");
}
StartPaymentStatusPolling();
}
private static BitmapImage BuildPaymentQrCodeImage(string codeUrl)
{
using var generator = new QRCodeGenerator();
using var qrCodeData = generator.CreateQrCode(codeUrl, QRCodeGenerator.ECCLevel.Q);
var qrCode = new PngByteQRCode(qrCodeData);
var pngBytes = qrCode.GetGraphic(20);
using var stream = new MemoryStream(pngBytes);
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
return image;
}
private void StartPaymentStatusPolling()
{
StopPaymentStatusPolling();
if (CurrentOrder == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
return;
}
_isPaymentPolling = true;
_paymentStatusTimer.Start();
_paymentCountdownTimer.Start();
}
private void StopPaymentStatusPolling()
{
_isPaymentPolling = false;
_paymentStatusTimer.Stop();
_paymentCountdownTimer.Stop();
_isCheckingPaymentStatus = false;
}
private void RefreshPaymentCountdown()
{
if (PaymentExpiresAt == null)
{
PaymentCountdownText = "";
return;
}
var remaining = PaymentExpiresAt.Value - DateTimeOffset.Now;
if (remaining <= TimeSpan.Zero)
{
PaymentCountdownText = "00:00";
return;
}
PaymentCountdownText = remaining.ToString(@"mm\:ss");
}
private async Task RefreshPaymentCountdownAsync()
{
RefreshPaymentCountdown();
if (IsPaymentExpired || PaymentExpiresAt == null || CurrentOrder == null || CurrentOrder.IsPaid)
{
return;
}
if (PaymentExpiresAt.Value <= DateTimeOffset.Now)
{
await HandlePaymentExpiredAsync();
}
}
private async Task PollPaymentStatusAsync()
{
if (_isCheckingPaymentStatus)
{
return;
}
if (!_isPaymentPolling || CurrentOrder == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
StopPaymentStatusPolling();
return;
}
try
{
_isCheckingPaymentStatus = true;
PaymentStatusText = "正在确认支付结果...";
var paymentStatus = await _apiService.GetPaymentStatusAsync(CurrentOrder.Id, PaymentOutTradeNo);
if (paymentStatus?.Order != null)
{
CurrentOrder = paymentStatus.Order;
}
if (paymentStatus?.IsPaid == true)
{
await HandlePaymentSuccessAsync();
}
else if (!IsPaymentExpired)
{
PaymentStatusText = "等待扫码支付";
}
}
catch (Exception ex)
{
_logger.LogError("轮询支付状态失败", ex);
if (!IsPaymentExpired)
{
PaymentStatusText = "支付状态查询异常";
}
}
finally
{
_isCheckingPaymentStatus = false;
}
}
private Task HandlePaymentSuccessAsync()
{
StopPaymentStatusPolling();
if (CurrentOrder == null)
{
return Task.CompletedTask;
}
IsPaymentExpired = false;
PaymentStatusText = "支付成功";
PaymentCountdownText = "";
PaymentExpiresAt = null;
StatusMessage = "支付成功!设备门已打开,请将宠物放入";
IsDoorOpen = true;
_logger.LogInfo("支付成功,门已打开");
MessageBox.Show("支付成功!\n\n设备门已自动打开\n请将宠物放入设备后关闭门开始洗护",
"支付成功", MessageBoxButton.OK, MessageBoxImage.Information);
CurrentView = "Idle";
ViewChanged?.Invoke("Idle");
return Task.CompletedTask;
}
private async Task HandlePaymentExpiredAsync()
{
if (IsPaymentExpired)
{
return;
}
IsPaymentExpired = true;
StopPaymentStatusPolling();
PaymentStatusText = "二维码已超时,请重新下单";
PaymentCountdownText = "00:00";
PaymentQrCodeImage = null;
StatusMessage = "支付超时,请重新选择套餐";
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
try
{
CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Cancelled);
}
catch (Exception ex)
{
_logger.LogError("支付超时后取消订单失败", ex);
}
}
}
private async Task CancelCurrentPaymentAsync(string statusMessage)
{
StopPaymentStatusPolling();
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
try
{
CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Cancelled);
}
catch (Exception ex)
{
_logger.LogError("取消支付时更新订单状态失败", ex);
}
}
ResetPaymentSession();
CurrentView = "Payment";
ViewChanged?.Invoke("Payment");
StatusMessage = statusMessage;
}
private void ResetPaymentSession()
{
CurrentOrder = null;
SelectedPackage = null;
PaymentCodeUrl = "";
PaymentOutTradeNo = "";
PaymentExpiresAt = null;
PaymentCountdownText = "";
PaymentStatusText = "等待扫码支付";
IsPaymentExpired = false;
PaymentQrCodeImage = null;
}
[RelayCommand]
private async Task CloseDoorAsync()
{
if (CurrentOrder == null || !IsDoorOpen)
{
MessageBox.Show("门未打开", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
_logger.LogInfo($"关门并开始洗护订单ID: {CurrentOrder.Id}");
// 触发 Modbus M80 复归型按钮
// await _modbusService.TriggerStartWashAsync();
// 通过MQTT发送关门状态
await _mqttService.PublishAsync("device/status", new
{
status = "door_closed",
orderId = CurrentOrder.Id,
timestamp = DateTime.Now
});
// 更新订单状态
CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.DoorClosed);
IsDoorOpen = false;
StatusMessage = "门已关闭,清洗即将开始...";
_logger.LogInfo("门已关闭,等待清洗开始");
// 切换到洗护界面
CurrentView = "Washing";
ViewChanged?.Invoke("Washing");
IsWashing = true;
CurrentStep = "第一次冲水";
WashProgress = 0;
// 开始模拟洗护流程
_ = SimulateWashingProcessAsync();
}
catch (Exception ex)
{
_logger.LogError("关门失败", ex);
StatusMessage = $"关门失败: {ex.Message}";
MessageBox.Show($"关门失败\n\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
[RelayCommand]
private async Task EmergencyStopAsync()
{
try
{
_logger.LogInfo("触发紧急停止");
// 触发 Modbus M83 复归型按钮
await _modbusService.TriggerEmergencyStopAsync();
// 停止洗护流程
IsWashing = false;
// 通过MQTT发送紧急停止状态
await _mqttService.PublishAsync("device/status", new
{
status = "emergency_stop",
orderId = CurrentOrder?.Id,
timestamp = DateTime.Now
});
StatusMessage = "紧急停止已触发";
_logger.LogInfo("紧急停止执行成功");
MessageBox.Show("紧急停止已触发!\n\n设备已停止运行",
"紧急停止", MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch (Exception ex)
{
_logger.LogError("紧急停止失败", ex);
StatusMessage = $"紧急停止失败: {ex.Message}";
MessageBox.Show($"紧急停止失败\n\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task SimulateWashingProcessAsync()
{
try
{
// 将分钟转换为秒
var steps = new[]
{
("第一次冲水", FirstSprayWaterTime * 60),
("沐浴露喷洒", SprayShampoo1Time * 60),
("第二次冲水", AfterShampoo1SprayTime * 60),
("香波喷洒", SprayShampoo2Time * 60),
("第三次冲水", AfterShampoo2SprayTime * 60),
("护理液喷洒", SprayShampoo3Time * 60),
("第四次冲水", AfterShampoo3SprayTime * 60),
("热风吹毛", HotAirTime * 60),
("冷热风混合", ColdAirTime * 60)
// 紫外线杀菌不在洗护流程中
};
int totalDuration = steps.Sum(s => s.Item2);
int elapsed = 0;
for (int stepIndex = 0; stepIndex < steps.Length; stepIndex++)
{
var (stepName, duration) = steps[stepIndex];
CurrentStep = stepName;
_logger.LogInfo($"开始步骤: {stepName}, 时长: {duration / 60}分钟");
// 更新步骤状态
Application.Current.Dispatcher.Invoke(() =>
{
// 标记之前的步骤为已完成
for (int i = 0; i < stepIndex; i++)
{
WashSteps[i].Status = "已完成";
WashSteps[i].IsActive = false;
}
// 标记当前步骤为进行中
WashSteps[stepIndex].Status = "进行中";
WashSteps[stepIndex].IsActive = true;
// 标记后续步骤为等待中
for (int i = stepIndex + 1; i < WashSteps.Count; i++)
{
WashSteps[i].Status = "等待中";
WashSteps[i].IsActive = false;
}
});
for (int i = 0; i <= duration; i++)
{
if (!IsWashing) return; // 如果被停止,退出
elapsed++;
WashProgress = (int)((double)elapsed / totalDuration * 100);
int remaining = totalDuration - elapsed;
int minutes = remaining / 60;
int seconds = remaining % 60;
RemainingTime = $"{minutes:D2}:{seconds:D2}";
// 模拟温度变化
UpdateTemperatures(stepName, i, duration);
await Task.Delay(100); // 加速模拟实际应为1000ms
}
}
// 标记所有步骤为已完成
Application.Current.Dispatcher.Invoke(() =>
{
foreach (var step in WashSteps)
{
step.Status = "已完成";
step.IsActive = false;
}
});
// 洗护流程完成,自动开门
_logger.LogInfo("洗护流程完成,自动开门");
IsDoorOpen = true;
StatusMessage = "洗护完成,请取走宠物";
CurrentStep = "请取走宠物";
// 等待门关闭(模拟宠物取走后关门)
await WaitForDoorCloseAsync();
}
catch (Exception ex)
{
_logger.LogError("洗护流程失败", ex);
StatusMessage = $"洗护流程失败: {ex.Message}";
}
}
/// <summary>
/// 等待门关闭(宠物取走后)
/// </summary>
private async Task WaitForDoorCloseAsync()
{
_logger.LogInfo("等待门关闭...");
// 在实际生产环境中,这里应该监听门传感器信号
// 这里模拟等待10秒后自动关门
await Task.Delay(10000);
IsDoorOpen = false;
_logger.LogInfo("门已关闭,开始清理和杀菌流程");
// 门关闭后,开始清理和杀菌流程
await StartCleaningAndSterilizationAsync();
}
/// <summary>
/// 清理和紫外线杀菌流程(独立于洗护流程)
/// 关门后冲水2分钟清理笼子内多余狗毛同时紫外线灯开启杀菌
/// </summary>
private async Task StartCleaningAndSterilizationAsync()
{
try
{
// 显示清理弹窗(不可手动关闭)
IsCleaningDialogOpen = true;
CleaningProgress = 0;
_logger.LogInfo("开始清理和杀菌流程");
// 冲水2分钟 + 紫外线杀菌时间
int flushDuration = 2 * 60; // 2分钟冲水
int uvDuration = UvSterilizationTime * 60; // 紫外线杀菌时间
int totalDuration = flushDuration + uvDuration;
// 第一阶段冲水2分钟清理狗毛同时开启紫外线灯
_logger.LogInfo("开始冲水清理狗毛,同时开启紫外线灯");
CleaningMessage = "正在冲水清理笼子...";
for (int i = 0; i <= flushDuration; i++)
{
CleaningProgress = (int)((double)i / totalDuration * 100);
await Task.Delay(100);
}
// 第二阶段:继续紫外线杀菌
_logger.LogInfo("冲水完成,继续紫外线杀菌");
CleaningMessage = "正在紫外线杀菌...";
for (int i = flushDuration; i <= totalDuration; i++)
{
CleaningProgress = (int)((double)i / totalDuration * 100);
await Task.Delay(100);
}
CleaningProgress = 100;
CleaningMessage = "清理完成!";
await Task.Delay(1000);
// 关闭清理弹窗
IsCleaningDialogOpen = false;
_logger.LogInfo("清理和杀菌流程完成");
// 全部完成,返回待机界面
await CompleteWashingAsync();
}
catch (Exception ex)
{
_logger.LogError("清理和杀菌流程失败", ex);
StatusMessage = $"清理失败: {ex.Message}";
IsCleaningDialogOpen = false;
}
}
private async Task CompleteWashingAsync()
{
if (CurrentOrder == null) return;
try
{
_logger.LogInfo($"清洗完成订单ID: {CurrentOrder.Id}");
// 发送清洗完成状态
await _mqttService.PublishAsync("device/status", new
{
status = "completed",
orderId = CurrentOrder.Id,
timestamp = DateTime.Now
});
CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Completed);
IsWashing = false;
IsDoorOpen = true;
StatusMessage = "清洗完成!请取出宠物";
// 显示自动关闭的完成对话框
var dialog = new Views.CompletionDialog();
dialog.ShowDialog();
_logger.LogInfo("订单流程完成");
// 返回待机界面
CurrentOrder = null;
SelectedPackage = null;
BackToIdle();
}
catch (Exception ex)
{
_logger.LogError("完成流程失败", ex);
StatusMessage = $"完成流程失败: {ex.Message}";
}
}
private void OnMqttMessageReceived(string topic, string payload)
{
Application.Current.Dispatcher.Invoke(() =>
{
try
{
_logger.LogInfo($"收到MQTT消息 - Topic: {topic}, Payload: {payload}");
var message = JsonSerializer.Deserialize<JsonElement>(payload);
if (topic == "device/command")
{
var command = message.GetProperty("command").GetString();
if (command == "open_door")
{
IsDoorOpen = true;
StatusMessage = "设备门已打开,请将宠物放入后点击关门";
_logger.LogInfo("收到开门指令");
}
else if (command == "start_wash")
{
IsWashing = true;
var duration = message.GetProperty("durationMinutes").GetInt32();
StatusMessage = $"清洗已开始,预计 {duration} 分钟完成";
_logger.LogInfo($"收到开始清洗指令,时长: {duration}分钟");
}
}
else if (topic == "device/status")
{
var status = message.GetProperty("status").GetString();
StatusMessage = $"设备状态: {status}";
_logger.LogInfo($"设备状态更新: {status}");
}
}
catch (Exception ex)
{
_logger.LogError("处理MQTT消息失败", ex);
StatusMessage = $"处理消息失败: {ex.Message}";
}
});
}
private void OnModbusConnectionStatusChanged(bool isConnected)
{
Application.Current.Dispatcher.Invoke(() =>
{
IsModbusConnected = isConnected;
if (isConnected)
{
_logger.LogInfo("Modbus TCP 设备已连接");
if (StatusMessage.Contains("设备未连接"))
{
StatusMessage = "系统就绪,设备已连接";
}
}
else
{
_logger.LogWarning("Modbus TCP 设备连接断开");
if (!StatusMessage.Contains("失败") && !StatusMessage.Contains("错误"))
{
StatusMessage = "警告:设备连接断开";
}
}
});
}
[RelayCommand]
private void OpenPackageManagement()
{
if (Packages.Count == 0)
{
StatusMessage = "套餐列表未加载";
return;
}
EditingPackage ??= Packages[0];
OpenPackageManagementRequested?.Invoke();
}
[RelayCommand]
private async Task SavePackageConfigAsync()
{
if (EditingPackage == null)
{
MessageBox.Show("请先选择要编辑的套餐。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
MessageBox.Show("当前有未完成支付订单,不能修改套餐配置。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (EditingPackagePrice <= 0 ||
EditingPackageFirstSprayWaterTime <= 0 ||
EditingPackageAfterShampoo1SprayTime <= 0 ||
EditingPackageAfterShampoo2SprayTime <= 0 ||
EditingPackageAfterShampoo3SprayTime <= 0 ||
EditingPackageSprayShampoo1Time <= 0 ||
EditingPackageSprayShampoo2Time <= 0 ||
EditingPackageSprayShampoo3Time <= 0 ||
EditingPackageHotAirTime <= 0 ||
EditingPackageColdAirTime <= 0 ||
EditingPackageUvSterilizationTime <= 0)
{
MessageBox.Show("套餐金额和每个流程时间都必须大于 0。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
StatusMessage = $"正在保存 {EditingPackage.Name} 配置...";
var packageToSave = new Package
{
Id = EditingPackage.Id,
Name = EditingPackage.Name,
Description = EditingPackage.Description,
Price = EditingPackagePrice,
DurationMinutes = EditingPackageTotalDuration,
FirstSprayWaterTime = EditingPackageFirstSprayWaterTime,
AfterShampoo1SprayTime = EditingPackageAfterShampoo1SprayTime,
AfterShampoo2SprayTime = EditingPackageAfterShampoo2SprayTime,
AfterShampoo3SprayTime = EditingPackageAfterShampoo3SprayTime,
SprayShampoo1Time = EditingPackageSprayShampoo1Time,
SprayShampoo2Time = EditingPackageSprayShampoo2Time,
SprayShampoo3Time = EditingPackageSprayShampoo3Time,
HotAirTime = EditingPackageHotAirTime,
ColdAirTime = EditingPackageColdAirTime,
UvSterilizationTime = EditingPackageUvSterilizationTime
};
var savedPackage = await _apiService.UpdatePackageAsync(packageToSave)
?? throw new InvalidOperationException("Server did not return the updated package.");
ReplacePackageInCollection(savedPackage);
EditingPackage = savedPackage;
if (SelectedPackage?.Id == savedPackage.Id)
{
SelectedPackage = savedPackage;
await ApplyPackageTimingProfileAsync(savedPackage);
}
if (CurrentOrder?.PackageId == savedPackage.Id && CurrentOrder.IsPaid)
{
CurrentOrder.Package = savedPackage;
}
StatusMessage = $"{savedPackage.Name} 配置已保存";
MessageBox.Show(
$"{savedPackage.Name} 已更新。\n\n金额: {savedPackage.DisplayPrice} 元\n总时长: {savedPackage.DurationMinutes} 分钟",
"保存成功",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger.LogError("保存套餐配置失败", ex);
StatusMessage = $"保存套餐配置失败: {ex.Message}";
MessageBox.Show($"保存套餐配置失败\n\n{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
partial void OnEditingPackageChanged(Package? value)
{
if (value != null)
{
LoadEditingPackage(value);
}
}
partial void OnEditingPackageFirstSprayWaterTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageAfterShampoo1SprayTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageAfterShampoo2SprayTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageAfterShampoo3SprayTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageSprayShampoo1TimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageSprayShampoo2TimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageSprayShampoo3TimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageHotAirTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageColdAirTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageUvSterilizationTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
private void ReplacePackageInCollection(Package updatedPackage)
{
for (var i = 0; i < Packages.Count; i++)
{
if (Packages[i].Id == updatedPackage.Id)
{
Packages[i] = updatedPackage;
return;
}
}
Packages.Add(updatedPackage);
}
private void LoadEditingPackage(Package package)
{
EditingPackagePrice = package.Price;
EditingPackageFirstSprayWaterTime = package.FirstSprayWaterTime;
EditingPackageAfterShampoo1SprayTime = package.AfterShampoo1SprayTime;
EditingPackageAfterShampoo2SprayTime = package.AfterShampoo2SprayTime;
EditingPackageAfterShampoo3SprayTime = package.AfterShampoo3SprayTime;
EditingPackageSprayShampoo1Time = package.SprayShampoo1Time;
EditingPackageSprayShampoo2Time = package.SprayShampoo2Time;
EditingPackageSprayShampoo3Time = package.SprayShampoo3Time;
EditingPackageHotAirTime = package.HotAirTime;
EditingPackageColdAirTime = package.ColdAirTime;
EditingPackageUvSterilizationTime = package.UvSterilizationTime;
OnPropertyChanged(nameof(EditingPackageTotalDuration));
}
private void UpdateTemperatures(string stepName, int currentTime, int totalTime)
{
// 根据不同步骤模拟温度变化
var random = new Random();
double progress = (double)currentTime / totalTime;
switch (stepName)
{
case "第一次冲水":
case "第二次冲水":
case "第三次冲水":
case "第四次冲水":
// 冲水阶段水温在38-42度之间波动
WaterTemperature = 40.0 + (random.NextDouble() - 0.5) * 4;
RoomTemperature = 25.0 + (random.NextDouble() - 0.5) * 2;
break;
case "沐浴露喷洒":
case "香波喷洒":
case "护理液喷洒":
// 喷洒阶段:水温略低
WaterTemperature = 38.0 + (random.NextDouble() - 0.5) * 2;
RoomTemperature = 25.0 + (random.NextDouble() - 0.5) * 2;
break;
case "热风吹毛":
// 热风阶段:水温降低,室温升高
WaterTemperature = 35.0 - progress * 10 + (random.NextDouble() - 0.5) * 2;
RoomTemperature = 25.0 + progress * 8 + (random.NextDouble() - 0.5) * 2;
break;
case "冷热风混合":
// 混合风阶段:温度逐渐降低
WaterTemperature = 25.0 + (random.NextDouble() - 0.5) * 2;
RoomTemperature = 30.0 - progress * 5 + (random.NextDouble() - 0.5) * 2;
break;
case "紫外线杀菌":
// 紫外线杀菌阶段:温度稳定
WaterTemperature = 25.0 + (random.NextDouble() - 0.5) * 1;
RoomTemperature = 26.0 + (random.NextDouble() - 0.5) * 1;
break;
default:
// 默认温度
WaterTemperature = 40.0 + (random.NextDouble() - 0.5) * 2;
RoomTemperature = 25.0 + (random.NextDouble() - 0.5) * 2;
break;
}
// 确保温度在合理范围内
WaterTemperature = Math.Max(20, Math.Min(45, WaterTemperature));
RoomTemperature = Math.Max(20, Math.Min(35, RoomTemperature));
}
}