feat: add 客户端

This commit is contained in:
GukSang.Jin
2026-02-25 15:43:47 +08:00
parent 833031864b
commit aac0dd8fec
16 changed files with 888 additions and 39 deletions

View File

@@ -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>

View 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();
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View 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
}

View 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;
}

View File

@@ -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>

View 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>

View 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);
}
}
}

View 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;
}

View 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
{
// 忽略日志写入失败
}
}
}

View 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
{
// 重连失败,等待下次重试
}
}
}

View 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}";
}
});
}
}

View 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>

View 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();
}
}