feat: add 客户端
This commit is contained in:
@@ -2,8 +2,16 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PetWashControl"
|
||||
StartupUri="MainWindow.xaml">
|
||||
xmlns:converters="clr-namespace:PetWashControl.Converters"
|
||||
StartupUri="Views/MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Resources/Styles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<converters:BoolToStatusConverter x:Key="BoolToStatusConverter"/>
|
||||
<converters:BoolToWashingConverter x:Key="BoolToWashingConverter"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
38
PetWashControl/Converters/BoolToStatusConverter.cs
Normal file
38
PetWashControl/Converters/BoolToStatusConverter.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace PetWashControl.Converters;
|
||||
|
||||
public class BoolToStatusConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool isOpen)
|
||||
{
|
||||
return isOpen ? "已打开" : "已关闭";
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class BoolToWashingConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool isWashing)
|
||||
{
|
||||
return isWashing ? "清洗中" : "空闲";
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<Window x:Class="PetWashControl.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:PetWashControl"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace PetWashControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
PetWashControl/Models/Order.cs
Normal file
23
PetWashControl/Models/Order.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace PetWashControl.Models;
|
||||
|
||||
public class Order
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int PackageId { get; set; }
|
||||
public Package? Package { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public OrderStatus Status { get; set; }
|
||||
public bool IsPaid { get; set; }
|
||||
}
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Created,
|
||||
WaitingPayment,
|
||||
Paid,
|
||||
DoorOpened,
|
||||
DoorClosed,
|
||||
Washing,
|
||||
Completed,
|
||||
Cancelled
|
||||
}
|
||||
10
PetWashControl/Models/Package.cs
Normal file
10
PetWashControl/Models/Package.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace PetWashControl.Models;
|
||||
|
||||
public class Package
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal Price { get; set; }
|
||||
public int DurationMinutes { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -8,4 +8,12 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="MQTTnet" Version="4.3.7.1207" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="8.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
96
PetWashControl/Resources/Styles.xaml
Normal file
96
PetWashControl/Resources/Styles.xaml
Normal file
@@ -0,0 +1,96 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 颜色定义 -->
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#FF6B35"/>
|
||||
<SolidColorBrush x:Key="SecondaryBrush" Color="#004E89"/>
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#1A659E"/>
|
||||
<SolidColorBrush x:Key="BackgroundBrush" Color="#F7F7F7"/>
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="BorderBrush" Color="#DDDDDD"/>
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#333333"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#666666"/>
|
||||
|
||||
<!-- 按钮样式 -->
|
||||
<Style x:Key="PrimaryButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="20,10"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="5"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#FF8555"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#E55A25"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="#CCCCCC"/>
|
||||
<Setter Property="Foreground" Value="#999999"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
|
||||
<Setter Property="Background" Value="{StaticResource SecondaryBrush}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#0066AA"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#003D6B"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- GroupBox样式 -->
|
||||
<Style TargetType="GroupBox">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10"/>
|
||||
<Setter Property="Margin" Value="0,10"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
|
||||
<!-- ListBox样式 -->
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||
</Style>
|
||||
|
||||
<!-- TextBlock标题样式 -->
|
||||
<Style x:Key="TitleTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="28"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SubtitleTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="18"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="BodyTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
88
PetWashControl/Services/ApiService.cs
Normal file
88
PetWashControl/Services/ApiService.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using PetWashControl.Models;
|
||||
|
||||
namespace PetWashControl.Services;
|
||||
|
||||
public class ApiService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ConfigurationService _config;
|
||||
|
||||
public ApiService(ConfigurationService? config = null)
|
||||
{
|
||||
_config = config ?? new ConfigurationService();
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_config.ApiBaseUrl),
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<Package>> GetPackagesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<List<Package>>("api/packages")
|
||||
?? new List<Package>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new Exception($"无法连接到服务器: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Order?> CreateOrderAsync(int packageId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("api/orders", new { packageId });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<Order>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new Exception($"创建订单失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Order?> ConfirmPaymentAsync(int orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsync($"api/orders/{orderId}/payment", null);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<Order>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new Exception($"确认支付失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Order?> GetOrderAsync(int orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<Order>($"api/orders/{orderId}");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new Exception($"获取订单失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Order?> UpdateOrderStatusAsync(int orderId, OrderStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"api/orders/{orderId}/status", new { status });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<Order>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new Exception($"更新订单状态失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
PetWashControl/Services/ConfigurationService.cs
Normal file
12
PetWashControl/Services/ConfigurationService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace PetWashControl.Services;
|
||||
|
||||
public class ConfigurationService
|
||||
{
|
||||
public string ApiBaseUrl { get; set; } = "https://localhost:7001/";
|
||||
public string MqttBrokerHost { get; set; } = "localhost";
|
||||
public int MqttBrokerPort { get; set; } = 1883;
|
||||
public string MqttClientId { get; set; } = "PetWashControl";
|
||||
|
||||
public int PaymentCheckIntervalSeconds { get; set; } = 2;
|
||||
public int WashSimulationSeconds { get; set; } = 10;
|
||||
}
|
||||
50
PetWashControl/Services/LogService.cs
Normal file
50
PetWashControl/Services/LogService.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace PetWashControl.Services;
|
||||
|
||||
public class LogService
|
||||
{
|
||||
private readonly string _logFilePath;
|
||||
|
||||
public LogService()
|
||||
{
|
||||
var logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
_logFilePath = Path.Combine(logDirectory, $"log_{DateTime.Now:yyyyMMdd}.txt");
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
Log("INFO", message);
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
Log("WARN", message);
|
||||
}
|
||||
|
||||
public void LogError(string message, Exception? ex = null)
|
||||
{
|
||||
var fullMessage = ex != null ? $"{message}\n{ex}" : message;
|
||||
Log("ERROR", fullMessage);
|
||||
}
|
||||
|
||||
private void Log(string level, string message)
|
||||
{
|
||||
var logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
|
||||
|
||||
// 输出到调试窗口
|
||||
Debug.WriteLine(logMessage);
|
||||
|
||||
// 写入文件
|
||||
try
|
||||
{
|
||||
File.AppendAllText(_logFilePath, logMessage + Environment.NewLine);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略日志写入失败
|
||||
}
|
||||
}
|
||||
}
|
||||
109
PetWashControl/Services/MqttClientService.cs
Normal file
109
PetWashControl/Services/MqttClientService.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using MQTTnet;
|
||||
using MQTTnet.Client;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PetWashControl.Services;
|
||||
|
||||
public class MqttClientService
|
||||
{
|
||||
private readonly IMqttClient _mqttClient;
|
||||
private readonly MqttClientOptions _options;
|
||||
private readonly ConfigurationService _config;
|
||||
|
||||
public event Action<string, string>? MessageReceived;
|
||||
public bool IsConnected => _mqttClient.IsConnected;
|
||||
|
||||
public MqttClientService(ConfigurationService? config = null)
|
||||
{
|
||||
_config = config ?? new ConfigurationService();
|
||||
|
||||
var factory = new MqttFactory();
|
||||
_mqttClient = factory.CreateMqttClient();
|
||||
|
||||
_options = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer(_config.MqttBrokerHost, _config.MqttBrokerPort)
|
||||
.WithClientId(_config.MqttClientId)
|
||||
.WithCleanSession()
|
||||
.Build();
|
||||
|
||||
_mqttClient.ApplicationMessageReceivedAsync += OnMessageReceived;
|
||||
_mqttClient.DisconnectedAsync += OnDisconnected;
|
||||
}
|
||||
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
if (_mqttClient.IsConnected)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await _mqttClient.ConnectAsync(_options, CancellationToken.None);
|
||||
|
||||
// 订阅设备状态和命令主题
|
||||
var subscribeOptions = new MqttClientSubscribeOptionsBuilder()
|
||||
.WithTopicFilter("device/status")
|
||||
.WithTopicFilter("device/command")
|
||||
.Build();
|
||||
|
||||
await _mqttClient.SubscribeAsync(subscribeOptions, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"MQTT连接失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
if (_mqttClient.IsConnected)
|
||||
{
|
||||
await _mqttClient.DisconnectAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PublishAsync(string topic, object payload)
|
||||
{
|
||||
if (!_mqttClient.IsConnected)
|
||||
{
|
||||
throw new InvalidOperationException("MQTT客户端未连接");
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopic(topic)
|
||||
.WithPayload(json)
|
||||
.WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
.Build();
|
||||
|
||||
await _mqttClient.PublishAsync(message, CancellationToken.None);
|
||||
}
|
||||
|
||||
private Task OnMessageReceived(MqttApplicationMessageReceivedEventArgs args)
|
||||
{
|
||||
var topic = args.ApplicationMessage.Topic;
|
||||
var payloadBytes = args.ApplicationMessage.PayloadSegment.ToArray();
|
||||
var payload = Encoding.UTF8.GetString(payloadBytes);
|
||||
|
||||
MessageReceived?.Invoke(topic, payload);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnDisconnected(MqttClientDisconnectedEventArgs args)
|
||||
{
|
||||
// 自动重连
|
||||
if (!args.ClientWasConnected)
|
||||
return;
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
try
|
||||
{
|
||||
await ConnectAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 重连失败,等待下次重试
|
||||
}
|
||||
}
|
||||
}
|
||||
262
PetWashControl/ViewModels/MainViewModel.cs
Normal file
262
PetWashControl/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,262 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PetWashControl.Models;
|
||||
using PetWashControl.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
|
||||
namespace PetWashControl.ViewModels;
|
||||
|
||||
public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
private readonly ApiService _apiService;
|
||||
private readonly MqttClientService _mqttService;
|
||||
private readonly ConfigurationService _config;
|
||||
private readonly LogService _logger;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<Package> _packages = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private Package? _selectedPackage;
|
||||
|
||||
[ObservableProperty]
|
||||
private Order? _currentOrder;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = "请选择套餐";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isDoorOpen;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isWashing;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isConnected;
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
_config = new ConfigurationService();
|
||||
_logger = new LogService();
|
||||
_apiService = new ApiService(_config);
|
||||
_mqttService = new MqttClientService(_config);
|
||||
_mqttService.MessageReceived += OnMqttMessageReceived;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInfo("开始初始化系统...");
|
||||
|
||||
await _mqttService.ConnectAsync();
|
||||
IsConnected = _mqttService.IsConnected;
|
||||
_logger.LogInfo("MQTT连接成功");
|
||||
|
||||
await LoadPackagesAsync();
|
||||
StatusMessage = "系统就绪,请选择套餐";
|
||||
_logger.LogInfo("系统初始化完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("初始化失败", ex);
|
||||
StatusMessage = $"初始化失败: {ex.Message}";
|
||||
MessageBox.Show($"系统初始化失败,请检查后端服务是否启动。\n\n错误: {ex.Message}",
|
||||
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[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 async Task CreateOrderAsync()
|
||||
{
|
||||
if (SelectedPackage == null)
|
||||
{
|
||||
MessageBox.Show("请先选择套餐", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInfo($"创建订单,套餐ID: {SelectedPackage.Id}");
|
||||
CurrentOrder = await _apiService.CreateOrderAsync(SelectedPackage.Id);
|
||||
StatusMessage = $"订单创建成功,订单号: {CurrentOrder?.Id},请支付 ¥{SelectedPackage.Price}";
|
||||
_logger.LogInfo($"订单创建成功,订单ID: {CurrentOrder?.Id}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_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}");
|
||||
CurrentOrder = await _apiService.ConfirmPaymentAsync(CurrentOrder.Id);
|
||||
StatusMessage = "支付成功!设备门已打开,请将宠物放入";
|
||||
IsDoorOpen = true;
|
||||
_logger.LogInfo("支付成功,门已打开");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("支付失败", ex);
|
||||
StatusMessage = $"支付失败: {ex.Message}";
|
||||
MessageBox.Show($"支付失败\n\n{ex.Message}", "错误",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CloseDoorAsync()
|
||||
{
|
||||
if (CurrentOrder == null || !IsDoorOpen)
|
||||
{
|
||||
MessageBox.Show("门未打开", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInfo($"关门,订单ID: {CurrentOrder.Id}");
|
||||
|
||||
// 通过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("门已关闭,等待清洗开始");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("关门失败", ex);
|
||||
StatusMessage = $"关门失败: {ex.Message}";
|
||||
MessageBox.Show($"关门失败\n\n{ex.Message}", "错误",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
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}分钟");
|
||||
|
||||
// 模拟清洗完成
|
||||
Task.Delay(TimeSpan.FromSeconds(_config.WashSimulationSeconds)).ContinueWith(async _ =>
|
||||
{
|
||||
await SimulateWashCompleteAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
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 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}";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
153
PetWashControl/Views/MainWindow.xaml
Normal file
153
PetWashControl/Views/MainWindow.xaml
Normal file
@@ -0,0 +1,153 @@
|
||||
<Window x:Class="PetWashControl.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:PetWashControl.Views"
|
||||
mc:Ignorable="d"
|
||||
Title="无人自助洗宠机控制系统" Height="600" Width="900"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid Margin="20" Background="{StaticResource BackgroundBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextBlock Grid.Row="0" Text="无人自助洗宠机"
|
||||
Style="{StaticResource TitleTextBlock}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 套餐选择 -->
|
||||
<GroupBox Grid.Row="1" Header="选择套餐" Padding="10">
|
||||
<ListBox ItemsSource="{Binding Packages}" SelectedItem="{Binding SelectedPackage}"
|
||||
HorizontalContentAlignment="Stretch">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="15"
|
||||
Margin="5"
|
||||
Background="{StaticResource SurfaceBrush}"
|
||||
CornerRadius="8">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="#CCCCCC"
|
||||
Direction="270"
|
||||
ShadowDepth="2"
|
||||
BlurRadius="8"
|
||||
Opacity="0.3"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="{Binding Description}"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,5,0,0"
|
||||
TextWrapping="Wrap"/>
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,5,0,0">
|
||||
<Run Text="时长: "/>
|
||||
<Run Text="{Binding DurationMinutes}" FontWeight="SemiBold"/>
|
||||
<Run Text=" 分钟"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource PrimaryBrush}"
|
||||
Margin="20,0,0,0">
|
||||
<Run Text="¥"/>
|
||||
<Run Text="{Binding Price}"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,20,0,10">
|
||||
<Button Content="创建订单"
|
||||
Command="{Binding CreateOrderCommand}"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
Width="120"
|
||||
Height="40"
|
||||
Margin="5"/>
|
||||
<Button Content="模拟支付"
|
||||
Command="{Binding SimulatePaymentCommand}"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Width="120"
|
||||
Height="40"
|
||||
Margin="5"/>
|
||||
<Button Content="关门开始"
|
||||
Command="{Binding CloseDoorCommand}"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
Width="120"
|
||||
Height="40"
|
||||
Margin="5"
|
||||
IsEnabled="{Binding IsDoorOpen}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 状态显示 -->
|
||||
<Border Grid.Row="3"
|
||||
Background="{StaticResource SurfaceBrush}"
|
||||
Padding="15"
|
||||
CornerRadius="8"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="#CCCCCC"
|
||||
Direction="270"
|
||||
ShadowDepth="2"
|
||||
BlurRadius="8"
|
||||
Opacity="0.3"/>
|
||||
</Border.Effect>
|
||||
<StackPanel>
|
||||
<TextBlock Text="系统状态"
|
||||
FontSize="14"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,5"/>
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
FontSize="16"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<TextBlock Text="门状态: "
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<TextBlock Text="{Binding IsDoorOpen, Converter={StaticResource BoolToStatusConverter}}"
|
||||
FontSize="14"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentBrush}"/>
|
||||
|
||||
<TextBlock Text=" | 清洗状态: "
|
||||
FontSize="14"
|
||||
Margin="20,0,0,0"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<TextBlock Text="{Binding IsWashing, Converter={StaticResource BoolToWashingConverter}}"
|
||||
FontSize="14"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentBrush}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
22
PetWashControl/Views/MainWindow.xaml.cs
Normal file
22
PetWashControl/Views/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using PetWashControl.ViewModels;
|
||||
using System.Windows;
|
||||
|
||||
namespace PetWashControl.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _viewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = new MainViewModel();
|
||||
DataContext = _viewModel;
|
||||
Loaded += MainWindow_Loaded;
|
||||
}
|
||||
|
||||
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await _viewModel.InitializeAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user