From 434804a2c4c0d4032e86696a2f603d01301e1336 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Wed, 25 Feb 2026 18:30:24 +0800 Subject: [PATCH] feat: add ui --- PetWash.Api/petwash.db | Bin 0 -> 20480 bytes PetWashControl/App.xaml | 2 + .../Converters/BoolToStatusConverter.cs | 34 + PetWashControl/PetWashControl.csproj | 2 +- PetWashControl/Resources/Styles.xaml | 191 +++-- .../Services/ConfigurationService.cs | 2 +- PetWashControl/ViewModels/MainViewModel.cs | 301 ++++++-- PetWashControl/Views/MainWindow.xaml | 714 ++++++++++++++---- PetWashControl/Views/MainWindow.xaml.cs | 37 + 9 files changed, 1051 insertions(+), 232 deletions(-) create mode 100644 PetWash.Api/petwash.db diff --git a/PetWash.Api/petwash.db b/PetWash.Api/petwash.db new file mode 100644 index 0000000000000000000000000000000000000000..a6d9baff7ae04db67dfda590e892d3975e8487ca GIT binary patch literal 20480 zcmeI2%WoS+9LM)zCvg;Kxu7VANUKFiiPEliXJ(%Vz-%^=)xiXMHMdzL=h6D;=sRvI3W&Pu*2hkK!_6uezvoY*RBzqc<48hXLsj2 z^Zoti^BW&l_TppnrD`Cb-Y#zzt1{*8;{<_wM3y;@6W|kvPuN7biG>evEe%)|^%!JM=B29-a3)mM4d0Xi9orfsh4%&dR{I3|3BEOz?wa6BGPi(DV$m2(LoKr8CLD zaZp0Dzi?DCkXN50kSmwg`e`#eCk}8Aj`3%{yZv76>dW;z&(}Y` zt`kkw9;knEyLR`j*2wh1-mA4wKbo#zxpi=D|EJGh`u@Vj+O^BITW{B1f3fz#je{57 zeOLga`}rs9H!jsK?YEZM;4|JiaOl%E`1a2GZG(sSroj`nH|`$XywEZ*z(DF8sK0i# zcI9S!eeJ_n+Upzd|AJKKq+g)Jh6IoR5 z6U@*EFHZ2`-xI{f`~T3$JDhY`l18tOI?1n-&m?0ofDH*C0VIF~kN^@u0!RP}AOR%s z4-t5LD8cb5K9TY`k!Zv`u94%!kagQ7rmHiRn2tqhtXHyBi)n^KdldM&)-ah)ZF_eR zygfeQd7coRrduwhs_vME!_0wR%~Y9XQ`4|}Bt})uVU}eXU6SJxTeS>bvo*Rh5C-PD z0~6QKRb4lUX4_r6(m`V?wKQTA(&Y}4rRt0^!=x+mzV4b!nQA*GA&%4S4w?gzAw}Kl za@Sm@t2Uqp|CLxjcg=E%u3C-cSa6`^q zfS(EcG^$x7Qjw+xsav{EEen2)KRP5$i5akrIvQ-cL$X~>cc~6_)TqOZT_N0+7dd8d z4orxx8VoAZ6PVgAB`P&2B_zTOmf0>Ns%|>A7R3o5G?&2XbPQWJ*?GRthAC^*N++gn zS8EJw#l4f_&yIwUvkp#($CV5Qce0s z`bzo@2CyLkB!C2v01`j~NB{{S0VIF~kN^@u0{> + + diff --git a/PetWashControl/Converters/BoolToStatusConverter.cs b/PetWashControl/Converters/BoolToStatusConverter.cs index a63d58f..1df3073 100644 --- a/PetWashControl/Converters/BoolToStatusConverter.cs +++ b/PetWashControl/Converters/BoolToStatusConverter.cs @@ -36,3 +36,37 @@ public class BoolToWashingConverter : IValueConverter throw new NotImplementedException(); } } + +public class BoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; + } + return System.Windows.Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +public class InverseBoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? System.Windows.Visibility.Collapsed : System.Windows.Visibility.Visible; + } + return System.Windows.Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/PetWashControl/PetWashControl.csproj b/PetWashControl/PetWashControl.csproj index 76e59f8..d6e6372 100644 --- a/PetWashControl/PetWashControl.csproj +++ b/PetWashControl/PetWashControl.csproj @@ -1,4 +1,4 @@ - + WinExe diff --git a/PetWashControl/Resources/Styles.xaml b/PetWashControl/Resources/Styles.xaml index 8279cd0..87ee0c0 100644 --- a/PetWashControl/Resources/Styles.xaml +++ b/PetWashControl/Resources/Styles.xaml @@ -1,44 +1,67 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + diff --git a/PetWashControl/Services/ConfigurationService.cs b/PetWashControl/Services/ConfigurationService.cs index 689e6d6..e65a5db 100644 --- a/PetWashControl/Services/ConfigurationService.cs +++ b/PetWashControl/Services/ConfigurationService.cs @@ -2,7 +2,7 @@ namespace PetWashControl.Services; public class ConfigurationService { - public string ApiBaseUrl { get; set; } = "https://localhost:7001/"; + public string ApiBaseUrl { get; set; } = "https://localhost:7203/"; public string MqttBrokerHost { get; set; } = "localhost"; public int MqttBrokerPort { get; set; } = 1883; public string MqttClientId { get; set; } = "PetWashControl"; diff --git a/PetWashControl/ViewModels/MainViewModel.cs b/PetWashControl/ViewModels/MainViewModel.cs index d0753b2..7ca081b 100644 --- a/PetWashControl/ViewModels/MainViewModel.cs +++ b/PetWashControl/ViewModels/MainViewModel.cs @@ -8,6 +8,18 @@ using System.Windows; namespace PetWashControl.ViewModels; +public partial class WashStep : ObservableObject +{ + [ObservableProperty] + private string _name = ""; + + [ObservableProperty] + private string _status = "等待中"; + + [ObservableProperty] + private bool _isActive; +} + public partial class MainViewModel : ObservableObject { private readonly ApiService _apiService; @@ -15,6 +27,8 @@ public partial class MainViewModel : ObservableObject private readonly ConfigurationService _config; private readonly LogService _logger; + public event Action? ViewChanged; + [ObservableProperty] private ObservableCollection _packages = new(); @@ -25,7 +39,7 @@ public partial class MainViewModel : ObservableObject private Order? _currentOrder; [ObservableProperty] - private string _statusMessage = "请选择套餐"; + private string _statusMessage = "系统就绪"; [ObservableProperty] private bool _isDoorOpen; @@ -36,6 +50,21 @@ public partial class MainViewModel : ObservableObject [ObservableProperty] private bool _isConnected; + [ObservableProperty] + private string _currentView = "Idle"; + + [ObservableProperty] + private int _washProgress; + + [ObservableProperty] + private string _currentStep = ""; + + [ObservableProperty] + private string _remainingTime = "00:00"; + + [ObservableProperty] + private ObservableCollection _washSteps = new(); + public MainViewModel() { _config = new ConfigurationService(); @@ -43,6 +72,20 @@ public partial class MainViewModel : ObservableObject _apiService = new ApiService(_config); _mqttService = new MqttClientService(_config); _mqttService.MessageReceived += OnMqttMessageReceived; + + // 初始化洗护步骤 + InitializeWashSteps(); + } + + 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 = "等待中" }); } public async Task InitializeAsync() @@ -56,7 +99,7 @@ public partial class MainViewModel : ObservableObject _logger.LogInfo("MQTT连接成功"); await LoadPackagesAsync(); - StatusMessage = "系统就绪,请选择套餐"; + StatusMessage = "系统就绪,请点击开始"; _logger.LogInfo("系统初始化完成"); } catch (Exception ex) @@ -89,6 +132,74 @@ public partial class MainViewModel : ObservableObject } } + [RelayCommand] + private void ShowPayment() + { + CurrentView = "Payment"; + ViewChanged?.Invoke("Payment"); + StatusMessage = "请选择套餐"; + _logger.LogInfo("切换到支付界面"); + } + + [RelayCommand] + private void BackToIdle() + { + CurrentView = "Idle"; + ViewChanged?.Invoke("Idle"); + StatusMessage = "系统就绪,请点击开始"; + SelectedPackage = null; + CurrentOrder = null; + IsDoorOpen = false; + + // 重置洗护步骤状态 + foreach (var step in WashSteps) + { + step.Status = "等待中"; + step.IsActive = false; + } + + _logger.LogInfo("返回待机界面"); + } + + [RelayCommand] + private async Task SelectPackage(Package package) + { + if (package == null) return; + + SelectedPackage = package; + _logger.LogInfo($"选择套餐: {package.Name}"); + + try + { + // 创建订单 + CurrentOrder = await _apiService.CreateOrderAsync(package.Id); + StatusMessage = $"订单创建成功,请扫码支付 ¥{package.Price}"; + _logger.LogInfo($"订单创建成功,订单ID: {CurrentOrder?.Id}"); + + // 切换到二维码支付界面 + CurrentView = "QRCode"; + ViewChanged?.Invoke("QRCode"); + } + catch (Exception ex) + { + _logger.LogError("创建订单失败", ex); + StatusMessage = $"创建订单失败: {ex.Message}"; + MessageBox.Show($"创建订单失败\n\n{ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + [RelayCommand] + private void CancelPayment() + { + _logger.LogInfo("取消支付"); + CurrentOrder = null; + SelectedPackage = null; + CurrentView = "Payment"; + ViewChanged?.Invoke("Payment"); + StatusMessage = "已取消支付,请重新选择套餐"; + } + [RelayCommand] private async Task CreateOrderAsync() { @@ -126,10 +237,24 @@ public partial class MainViewModel : ObservableObject try { _logger.LogInfo($"模拟支付,订单ID: {CurrentOrder.Id}"); + + // 模拟支付处理延迟 + StatusMessage = "正在处理支付..."; + await Task.Delay(1500); + + // 确认支付 CurrentOrder = await _apiService.ConfirmPaymentAsync(CurrentOrder.Id); StatusMessage = "支付成功!设备门已打开,请将宠物放入"; IsDoorOpen = true; _logger.LogInfo("支付成功,门已打开"); + + // 显示支付成功提示 + MessageBox.Show("支付成功!\n\n设备门已自动打开\n请将宠物放入设备后关闭门开始洗护", + "支付成功", MessageBoxButton.OK, MessageBoxImage.Information); + + // 返回待机界面,显示关门按钮 + CurrentView = "Idle"; + ViewChanged?.Invoke("Idle"); } catch (Exception ex) { @@ -167,6 +292,16 @@ public partial class MainViewModel : ObservableObject IsDoorOpen = false; StatusMessage = "门已关闭,清洗即将开始..."; _logger.LogInfo("门已关闭,等待清洗开始"); + + // 切换到洗护界面 + CurrentView = "Washing"; + ViewChanged?.Invoke("Washing"); + IsWashing = true; + CurrentStep = "第一次冲水"; + WashProgress = 0; + + // 开始模拟洗护流程 + _ = SimulateWashingProcessAsync(); } catch (Exception ex) { @@ -177,6 +312,127 @@ public partial class MainViewModel : ObservableObject } } + private async Task SimulateWashingProcessAsync() + { + try + { + var steps = new[] + { + ("第一次冲水", 120), + ("沐浴露喷洒", 30), + ("第二次冲水", 180), + ("香波喷洒", 30), + ("第三次冲水", 180), + ("热风吹毛", 300), + ("冷热风混合", 120) + }; + + 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}"); + + // 更新步骤状态 + 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}"; + + await Task.Delay(100); // 加速模拟,实际应为1000ms + } + } + + // 标记所有步骤为已完成 + Application.Current.Dispatcher.Invoke(() => + { + foreach (var step in WashSteps) + { + step.Status = "已完成"; + step.IsActive = false; + } + }); + + // 洗护完成 + await CompleteWashingAsync(); + } + catch (Exception ex) + { + _logger.LogError("洗护流程失败", ex); + StatusMessage = $"洗护流程失败: {ex.Message}"; + } + } + + 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 = "清洗完成!请取出宠物"; + + MessageBox.Show("清洗完成!\n\n感谢使用无人自动洗宠机", "完成", + MessageBoxButton.OK, MessageBoxImage.Information); + + _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(() => @@ -202,12 +458,6 @@ public partial class MainViewModel : ObservableObject var duration = message.GetProperty("durationMinutes").GetInt32(); StatusMessage = $"清洗已开始,预计 {duration} 分钟完成"; _logger.LogInfo($"收到开始清洗指令,时长: {duration}分钟"); - - // 模拟清洗完成 - Task.Delay(TimeSpan.FromSeconds(_config.WashSimulationSeconds)).ContinueWith(async _ => - { - await SimulateWashCompleteAsync(); - }); } } else if (topic == "device/status") @@ -224,39 +474,4 @@ public partial class MainViewModel : ObservableObject } }); } - - private async Task SimulateWashCompleteAsync() - { - if (CurrentOrder == null) return; - - await Application.Current.Dispatcher.InvokeAsync(async () => - { - 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; - StatusMessage = "清洗完成!请取出宠物"; - - MessageBox.Show("清洗完成!感谢使用", "完成", - MessageBoxButton.OK, MessageBoxImage.Information); - _logger.LogInfo("订单流程完成"); - } - catch (Exception ex) - { - _logger.LogError("完成流程失败", ex); - StatusMessage = $"完成流程失败: {ex.Message}"; - } - }); - } } diff --git a/PetWashControl/Views/MainWindow.xaml b/PetWashControl/Views/MainWindow.xaml index 6959cf7..ffc6bdd 100644 --- a/PetWashControl/Views/MainWindow.xaml +++ b/PetWashControl/Views/MainWindow.xaml @@ -1,153 +1,601 @@ - - - - - - - - + Title="无人自动洗宠机智能控制系统" + Height="800" Width="1024" + WindowStyle="None" + ResizeMode="NoResize" + WindowStartupLocation="CenterScreen" + Background="{StaticResource BackgroundGradient}"> + + + + + + + + + + + - - + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +