D:删除Client和Server模块
This commit is contained in:
1 parent
5e9e9fa818
commit
b738a2b166
53 files changed
-5394
No files matched your search
@@ -1,9 +0,0 @@
|
|||||||
<Application x:Class="Client.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
|
||||||
<!-- HandyControl 主题资源已通过 AddSharpHandyControl() 自动注册,无需手动添加 -->
|
|
||||||
<Application.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Windows;
|
|
||||||
using Client.Services;
|
|
||||||
using Client.ViewModels;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Common;
|
|
||||||
using Common.Configuration;
|
|
||||||
using Config;
|
|
||||||
using NetClient.DependencyInjection;
|
|
||||||
using Sharp.Log.Extensions;
|
|
||||||
using Sharp.NetServer.DependencyInjection;
|
|
||||||
using Sharp.Wpf.Services;
|
|
||||||
using Sharp.Wpf.Abstractions;
|
|
||||||
using Sharp.Wpf.HandyControl.DependencyInjection;
|
|
||||||
using Sharp.Wpf.DevTools.DependencyInjection;
|
|
||||||
using Plc.Siemens.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Sharp.Process.DependencyInjection;
|
|
||||||
using Sharp.Python.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Client
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : Application
|
|
||||||
{
|
|
||||||
private ServiceProvider? _serviceProvider;
|
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnStartup(e);
|
|
||||||
|
|
||||||
// Global Exception Handling
|
|
||||||
DispatcherUnhandledException += (s, args) =>
|
|
||||||
{
|
|
||||||
// Safe logging: handle case where DI failed
|
|
||||||
if (_serviceProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
|
||||||
logger?.LogError(args.Exception, "UI Thread Unhandled Exception");
|
|
||||||
}
|
|
||||||
catch { /* Ignore logging failure during crash */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageBox.Show($"发生未处理异常: {args.Exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
args.Handled = true; // Prevent crash
|
|
||||||
};
|
|
||||||
|
|
||||||
TaskScheduler.UnobservedTaskException += (s, args) =>
|
|
||||||
{
|
|
||||||
if (_serviceProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
|
||||||
logger?.LogError(args.Exception, "Task Unobserved Exception");
|
|
||||||
}
|
|
||||||
catch { /* Ignore */ }
|
|
||||||
}
|
|
||||||
args.SetObserved();
|
|
||||||
};
|
|
||||||
|
|
||||||
AppDomain.CurrentDomain.UnhandledException += (s, args) =>
|
|
||||||
{
|
|
||||||
var ex = args.ExceptionObject as Exception;
|
|
||||||
if (_serviceProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
|
||||||
logger?.LogCritical(ex, "AppDomain Unhandled Exception (IsTerminating: {IsTerminating})", args.IsTerminating);
|
|
||||||
}
|
|
||||||
catch { /* Ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.IsTerminating)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"发生严重错误,程序即将退出: {ex?.Message}", "严重错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1) Build configuration manager from appsettings.json (+ reload on change + env vars)
|
|
||||||
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, reloadOnChange: true, includeEnvironment: true, basePath: AppContext.BaseDirectory);
|
|
||||||
|
|
||||||
// 2) Build DI container
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
|
|
||||||
// ConfigManager and IConfiguration
|
|
||||||
services.AddSingleton(cm);
|
|
||||||
// Register standard IConfiguration so that .BindConfiguration() works
|
|
||||||
services.AddSingleton<IConfiguration>(cm.Configuration);
|
|
||||||
|
|
||||||
// Logging (binds from section "SharpLog")
|
|
||||||
services.AddLogging(b => b.AddSharpLog());
|
|
||||||
|
|
||||||
// Net modules from configuration
|
|
||||||
services.AddNetClient(cm);
|
|
||||||
services.AddNetServer(cm);
|
|
||||||
|
|
||||||
// PLC
|
|
||||||
services.AddSiemensPlc(options =>
|
|
||||||
{
|
|
||||||
options.IpAddress = "127.0.0.1";
|
|
||||||
options.CpuType = S7.Net.CpuType.S71200;
|
|
||||||
// 配置监控循环频率 (单位: 毫秒)
|
|
||||||
// 默认 200ms。如果设为 100ms 则频率更高,但需注意网络负载。
|
|
||||||
options.MonitorInterval = 200;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process & Python
|
|
||||||
services.AddSharpProcess();
|
|
||||||
services.AddSharpPython();
|
|
||||||
|
|
||||||
// WPF app services
|
|
||||||
services.AddSharpHandyControl(); // HandyControl 对话框服务(也注册了 IDialogService)
|
|
||||||
services.AddSharpDevTools(); // 开发者工具(开发者工具、日志查看器、性能监控等)
|
|
||||||
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
|
||||||
services.AddSingleton<IUserSession, UserSession>();
|
|
||||||
// Register DispatcherService with the current UI dispatcher
|
|
||||||
services.AddSingleton<IDispatcher>(new DispatcherService(Dispatcher));
|
|
||||||
|
|
||||||
services.AddTransient<LoginViewModel>();
|
|
||||||
services.AddTransient<MainViewModel>();
|
|
||||||
services.AddTransient<PlcViewModel>();
|
|
||||||
services.AddTransient<ProcessTestViewModel>();
|
|
||||||
|
|
||||||
services.AddTransient<LoginWindow>();
|
|
||||||
services.AddTransient<MainWindow>();
|
|
||||||
services.AddTransient<PlcWindow>();
|
|
||||||
services.AddTransient<ProcessTestWindow>();
|
|
||||||
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
|
||||||
|
|
||||||
// 3) Register global facades for convenience
|
|
||||||
Global.RegisterSingleton<IServiceProvider>(_serviceProvider);
|
|
||||||
Global.RegisterSingleton(cm);
|
|
||||||
// Also register IDispatcher in Global if it's needed by static contexts (though injection is preferred)
|
|
||||||
Global.RegisterSingleton(_serviceProvider.GetRequiredService<IDispatcher>());
|
|
||||||
|
|
||||||
// 4) Show login window first, then main shell if authenticated
|
|
||||||
// Explicitly set ShutdownMode to prevent app from closing when LoginWindow closes
|
|
||||||
ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
|
||||||
|
|
||||||
var loginWindow = _serviceProvider.GetRequiredService<LoginWindow>();
|
|
||||||
var loginResult = loginWindow.ShowDialog();
|
|
||||||
|
|
||||||
if (loginResult == true)
|
|
||||||
{
|
|
||||||
var main = _serviceProvider.GetRequiredService<MainWindow>();
|
|
||||||
Current.MainWindow = main;
|
|
||||||
ShutdownMode = ShutdownMode.OnMainWindowClose;
|
|
||||||
main.Show();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
|
||||||
{
|
|
||||||
_serviceProvider?.Dispose();
|
|
||||||
base.OnExit(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
[assembly: ThemeInfo(
|
|
||||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// or application resource dictionaries)
|
|
||||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// app, or any theme specific resource dictionaries)
|
|
||||||
)]
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
|
||||||
<ProjectReference Include="..\Wpf.HandyControl\Wpf.HandyControl.csproj" />
|
|
||||||
<ProjectReference Include="..\Wpf.DevTools\Wpf.DevTools.csproj" />
|
|
||||||
<ProjectReference Include="..\Config\Config.csproj" />
|
|
||||||
<ProjectReference Include="..\NetClient\NetClient.csproj" />
|
|
||||||
<ProjectReference Include="..\NetServer\NetServer.csproj" />
|
|
||||||
<ProjectReference Include="..\Log\Log.csproj" />
|
|
||||||
<ProjectReference Include="..\Plc.Siemens\Plc.Siemens.csproj" />
|
|
||||||
<ProjectReference Include="..\Ports\Ports.csproj" />
|
|
||||||
<ProjectReference Include="..\Process\Process.csproj" />
|
|
||||||
<ProjectReference Include="..\Python\Python.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup />
|
|
||||||
</Project>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using Plc.Attributes;
|
|
||||||
|
|
||||||
namespace Client.Hardware;
|
|
||||||
|
|
||||||
// 必须继承 INotifyPropertyChanged 才能支持自动 UI 刷新
|
|
||||||
public interface IPackMachine : INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
[PlcAddress("DB1.DBW0")]
|
|
||||||
short RunningSpeed { get; set; }
|
|
||||||
|
|
||||||
[PlcAddress("DB1.DBW2")]
|
|
||||||
short Temperature { get; set; }
|
|
||||||
|
|
||||||
// 支持 bool (比如 M 区)
|
|
||||||
[PlcAddress("M100.0")]
|
|
||||||
bool IsStartButtonPushed { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace Client.Helpers;
|
|
||||||
|
|
||||||
public static class PasswordBoxHelper
|
|
||||||
{
|
|
||||||
public static readonly DependencyProperty BindablePasswordProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"BindablePassword",
|
|
||||||
typeof(string),
|
|
||||||
typeof(PasswordBoxHelper),
|
|
||||||
new PropertyMetadata(null, OnBindablePasswordChanged));
|
|
||||||
|
|
||||||
private static readonly DependencyProperty IsUpdatingProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"IsUpdating",
|
|
||||||
typeof(bool),
|
|
||||||
typeof(PasswordBoxHelper),
|
|
||||||
new PropertyMetadata(false));
|
|
||||||
|
|
||||||
public static string GetBindablePassword(DependencyObject obj) => (string)obj.GetValue(BindablePasswordProperty);
|
|
||||||
public static void SetBindablePassword(DependencyObject obj, string value) => obj.SetValue(BindablePasswordProperty, value);
|
|
||||||
|
|
||||||
private static bool GetIsUpdating(DependencyObject obj) => (bool)obj.GetValue(IsUpdatingProperty);
|
|
||||||
private static void SetIsUpdating(DependencyObject obj, bool value) => obj.SetValue(IsUpdatingProperty, value);
|
|
||||||
|
|
||||||
private static void OnBindablePasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (d is not PasswordBox passwordBox)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged -= HandlePasswordChanged;
|
|
||||||
|
|
||||||
if (!GetIsUpdating(passwordBox))
|
|
||||||
{
|
|
||||||
passwordBox.Password = e.NewValue?.ToString() ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged += HandlePasswordChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandlePasswordChanged(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not PasswordBox passwordBox)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged -= HandlePasswordChanged;
|
|
||||||
|
|
||||||
SetIsUpdating(passwordBox, true);
|
|
||||||
SetBindablePassword(passwordBox, passwordBox.Password);
|
|
||||||
SetIsUpdating(passwordBox, false);
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged += HandlePasswordChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Client.LoginWindow"
|
|
||||||
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:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
|
||||||
xmlns:h="clr-namespace:Client.Helpers"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="Sharp 客户端登录"
|
|
||||||
WindowStartupLocation="CenterScreen"
|
|
||||||
Width="420"
|
|
||||||
Height="420"
|
|
||||||
ResizeMode="NoResize">
|
|
||||||
<hc:GlowWindow.Resources>
|
|
||||||
<conv:BoolToVisibilityConverter x:Key="BoolToVisibility" />
|
|
||||||
</hc:GlowWindow.Resources>
|
|
||||||
<Grid Background="{DynamicResource RegionBrush}">
|
|
||||||
<Border CornerRadius="16" Padding="32" Background="{DynamicResource PopupBrush}">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="欢迎回来"
|
|
||||||
FontSize="24"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Margin="0,0,0,4" />
|
|
||||||
<TextBlock Text="请输入管理员账号信息"
|
|
||||||
Foreground="{DynamicResource SecondaryTextBrush}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Row="1" Margin="0,24,0,0">
|
|
||||||
<TextBlock Text="账号" FontWeight="SemiBold" />
|
|
||||||
<TextBox Margin="0,8,0,0"
|
|
||||||
Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
Height="36" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Row="2" Margin="0,12,0,0">
|
|
||||||
<TextBlock Text="密码" FontWeight="SemiBold" />
|
|
||||||
<PasswordBox Margin="0,8,0,0"
|
|
||||||
h:PasswordBoxHelper.BindablePassword="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
Height="36" />
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Grid.Row="3"
|
|
||||||
Margin="0,12,0,0"
|
|
||||||
Text="{Binding ErrorMessage}"
|
|
||||||
Foreground="Tomato"
|
|
||||||
Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}" />
|
|
||||||
<Button Grid.Row="4"
|
|
||||||
Margin="0,24,0,0"
|
|
||||||
Height="40"
|
|
||||||
Content="登 录"
|
|
||||||
Command="{Binding LoginCommand}" />
|
|
||||||
<ProgressBar Grid.Row="5"
|
|
||||||
Height="4"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="0,24,0,0"
|
|
||||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using System;
|
|
||||||
using HandyControl.Controls;
|
|
||||||
using Client.ViewModels;
|
|
||||||
|
|
||||||
namespace Client;
|
|
||||||
|
|
||||||
public partial class LoginWindow : GlowWindow
|
|
||||||
{
|
|
||||||
private readonly LoginViewModel _viewModel;
|
|
||||||
|
|
||||||
public LoginWindow(LoginViewModel viewModel)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
_viewModel = viewModel;
|
|
||||||
DataContext = viewModel;
|
|
||||||
_viewModel.LoginSucceeded += HandleLoginSucceeded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleLoginSucceeded(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
DialogResult = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnClosed(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnClosed(e);
|
|
||||||
_viewModel.LoginSucceeded -= HandleLoginSucceeded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Client.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:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
xmlns:w="clr-namespace:Sharp.Wpf.Windowing;assembly=Sharp.Wpf"
|
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="Sharp 控制台"
|
|
||||||
Height="560"
|
|
||||||
Width="960"
|
|
||||||
WindowStartupLocation="CenterScreen"
|
|
||||||
w:WindowPlacement.RememberPlacement="True"
|
|
||||||
w:WindowPlacement.PlacementId="MainWindow">
|
|
||||||
<hc:GlowWindow.Resources>
|
|
||||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
|
||||||
</hc:GlowWindow.Resources>
|
|
||||||
|
|
||||||
<!-- 快捷键绑定 -->
|
|
||||||
<hc:GlowWindow.InputBindings>
|
|
||||||
<KeyBinding Key="F12" Command="{Binding OpenDevToolsCommand}" />
|
|
||||||
<KeyBinding Key="F11" Command="{Binding OpenLogViewerCommand}" />
|
|
||||||
<KeyBinding Key="F10" Command="{Binding OpenPerformanceMonitorCommand}" />
|
|
||||||
</hc:GlowWindow.InputBindings>
|
|
||||||
<Grid Background="{DynamicResource RegionBrush}">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 菜单栏 -->
|
|
||||||
<Menu Grid.Row="0" Background="{DynamicResource RegionBrush}" Padding="8,4">
|
|
||||||
<MenuItem Header="文件(_F)">
|
|
||||||
<MenuItem Header="退出(_X)" InputGestureText="Alt+F4" Click="MenuItem_Exit_Click" />
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem Header="工具(_T)">
|
|
||||||
<MenuItem Header="PLC 调试(_P)" Command="{Binding OpenPlcToolCommand}" />
|
|
||||||
<MenuItem Header="串口调试(_S)" Command="{Binding OpenSerialPortToolCommand}" />
|
|
||||||
<MenuItem Header="Process/Python 测试(_T)" Command="{Binding OpenProcessTestCommand}" />
|
|
||||||
<Separator />
|
|
||||||
<MenuItem Header="开发者工具(_D)" Command="{Binding OpenDevToolsCommand}" InputGestureText="F12" />
|
|
||||||
<MenuItem Header="日志查看器(_L)" Command="{Binding OpenLogViewerCommand}" InputGestureText="F11" />
|
|
||||||
<MenuItem Header="性能监控(_M)" Command="{Binding OpenPerformanceMonitorCommand}" InputGestureText="F10" />
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem Header="帮助(_H)">
|
|
||||||
<MenuItem Header="关于(_A)" Command="{Binding OpenAboutCommand}" />
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="24">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Padding="24" CornerRadius="16">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="{Binding Greeting}" FontSize="26" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding StatusMessage}" Margin="0,6,0,0" Foreground="{DynamicResource SecondaryTextBrush}" />
|
|
||||||
<TextBlock Text="最后同步: " Margin="0,12,0,0" Foreground="{DynamicResource SecondaryTextBrush}">
|
|
||||||
<Run Text="{Binding LastSyncTime, StringFormat=yyyy-MM-dd HH:mm:ss}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
|
||||||
<Button Content="刷新仪表盘"
|
|
||||||
Padding="18,6"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
Command="{Binding RefreshDashboardCommand}" />
|
|
||||||
<Button Content="演示弹窗"
|
|
||||||
Padding="18,6"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
Command="{Binding ShowDialogCommand}" />
|
|
||||||
<Button Content="触发异常"
|
|
||||||
Padding="18,6"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
Command="{Binding ThrowCommand}" />
|
|
||||||
<Button Content="Process/Python 测试"
|
|
||||||
Padding="18,6"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
Style="{StaticResource ButtonWarning}"
|
|
||||||
Command="{Binding OpenProcessTestCommand}" />
|
|
||||||
<Button Content="串口调试"
|
|
||||||
Padding="18,6"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
Style="{StaticResource ButtonInfo}"
|
|
||||||
Command="{Binding OpenSerialPortToolCommand}" />
|
|
||||||
<Button Content="PLC 调试"
|
|
||||||
Padding="18,6"
|
|
||||||
Style="{StaticResource ButtonPrimary}"
|
|
||||||
Command="{Binding OpenPlcToolCommand}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="0,24,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="2*" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<UniformGrid Columns="2" Margin="0,0,16,0">
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="登录用户" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding UserDisplayName}" FontSize="20" Margin="0,8,0,0" />
|
|
||||||
<TextBlock Text="当前会话已建立"
|
|
||||||
Foreground="{DynamicResource SecondaryTextBrush}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="系统健康度" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding StatusMessage}" Margin="0,8,0,0" />
|
|
||||||
<ProgressBar Height="6" Margin="0,12,0,0"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
Visibility="{Binding IsBusy, Converter={StaticResource Bool2Vis}}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="上次操作" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding Greeting}" Margin="0,8,0,0" TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="包装机状态 (PLC)" FontWeight="SemiBold" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
|
||||||
<TextBlock Text="速度: " Foreground="{DynamicResource SecondaryTextBrush}"/>
|
|
||||||
<TextBlock Text="{Binding Machine.RunningSpeed}" FontWeight="Bold" />
|
|
||||||
<TextBlock Text=" rpm" Foreground="{DynamicResource SecondaryTextBrush}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
|
|
||||||
<TextBlock Text="温度: " Foreground="{DynamicResource SecondaryTextBrush}"/>
|
|
||||||
<TextBlock Text="{Binding Machine.Temperature}" FontWeight="Bold" />
|
|
||||||
<TextBlock Text=" ℃" Foreground="{DynamicResource SecondaryTextBrush}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- 交互控制演示 -->
|
|
||||||
<Separator Margin="0,10" Background="{DynamicResource BorderBrush}"/>
|
|
||||||
<TextBlock Text="控制面板" FontWeight="SemiBold" Margin="0,0,0,5"/>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="设定速度:" VerticalAlignment="Center"/>
|
|
||||||
<Slider Grid.Column="1" Minimum="0" Maximum="2000" Value="{Binding Machine.RunningSpeed}" Margin="10,0" IsMoveToPointEnabled="True"/>
|
|
||||||
<TextBlock Grid.Column="2" Text="{Binding Machine.RunningSpeed}" Width="40" TextAlignment="Right"/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<Button Content="启动机器" Command="{Binding StartMachineCommand}" Margin="0,0,10,0" Style="{StaticResource ButtonSuccess}"/>
|
|
||||||
<Button Content="停止机器" Command="{Binding StopMachineCommand}" Style="{StaticResource ButtonDanger}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<CheckBox IsChecked="{Binding Machine.IsStartButtonPushed}" Content="启动按钮状态 (双向绑定)" IsHitTestVisible="False"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="提示" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="使用 HandyControl 让界面更柔和" Margin="0,8,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</UniformGrid>
|
|
||||||
|
|
||||||
<Border Grid.Column="1" Background="{DynamicResource PopupBrush}" Padding="24" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="操作日志" FontSize="18" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="演示命令" Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,8,0,16" />
|
|
||||||
<ItemsControl>
|
|
||||||
<ItemsControl.Items>
|
|
||||||
<TextBlock Text="• 刷新仪表盘会模拟异步调用" Margin="0,4" />
|
|
||||||
<TextBlock Text="• 演示弹窗来自 Sharp.Wpf DialogService" Margin="0,4" />
|
|
||||||
<TextBlock Text="• 触发异常会被全局处理" Margin="0,4" />
|
|
||||||
<TextBlock Text="• 登录信息通过依赖注入管理" Margin="0,4" />
|
|
||||||
</ItemsControl.Items>
|
|
||||||
</ItemsControl>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using HandyControl.Controls;
|
|
||||||
using Client.ViewModels;
|
|
||||||
|
|
||||||
namespace Client;
|
|
||||||
|
|
||||||
public partial class MainWindow : GlowWindow
|
|
||||||
{
|
|
||||||
public MainWindow(MainViewModel viewModel)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MenuItem_Exit_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
Application.Current.Shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Client.PlcWindow"
|
|
||||||
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:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="PLC 调试工具" Height="500" Width="800"
|
|
||||||
WindowStartupLocation="CenterOwner">
|
|
||||||
<hc:GlowWindow.Resources>
|
|
||||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
|
||||||
</hc:GlowWindow.Resources>
|
|
||||||
|
|
||||||
<Grid Margin="20">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 连接控制 -->
|
|
||||||
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
|
||||||
<TextBlock Text="品牌:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox ItemsSource="{Binding Brands}" SelectedItem="{Binding SelectedBrand}" Width="120" Margin="0,0,15,0"/>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Visibility="{Binding IsSiemensOptionsVisible, Converter={StaticResource Bool2Vis}}">
|
|
||||||
<TextBlock Text="型号:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox ItemsSource="{Binding CpuTypes}" SelectedItem="{Binding SelectedCpuType}" Width="120" Margin="0,0,15,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBox hc:InfoElement.Placeholder="IP 地址" hc:InfoElement.Title="IP" Text="{Binding IpAddress}" Width="200" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="连接 / 启动服务" Command="{Binding ConnectCommand}" Margin="0,0,10,0" Style="{StaticResource ButtonPrimary}"/>
|
|
||||||
<Button Content="断开" Command="{Binding DisconnectCommand}"/>
|
|
||||||
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold" Foreground="{DynamicResource PrimaryTextBrush}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 单次读写 -->
|
|
||||||
<GroupBox Header="单次读写测试 (默认 Short)" Grid.Row="1" Margin="0,0,0,15">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Text="读取地址:" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Margin="5,0"/>
|
|
||||||
<Button Grid.Column="2" Content="读取" Command="{Binding ReadCommand}" Margin="5,0"/>
|
|
||||||
<TextBlock Grid.Column="3" Text="{Binding ReadValue}" VerticalAlignment="Center" Margin="5,0" FontWeight="Bold"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Text="写入地址:" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
|
||||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding WriteAddress}" Margin="5,10,0,0"/>
|
|
||||||
<TextBox Grid.Row="1" Grid.Column="3" Text="{Binding WriteValue}" Margin="5,10,0,0"/>
|
|
||||||
<Button Grid.Row="1" Grid.Column="4" Content="写入" Command="{Binding WriteCommand}" Margin="5,10,0,0"/>
|
|
||||||
</Grid>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 监控 -->
|
|
||||||
<GroupBox Header="实时监控 (Add Watch)" Grid.Row="2" Margin="0,0,0,15">
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="10">
|
|
||||||
<TextBox hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)" Text="{Binding MonitorAddress}" Width="200" Margin="0,0,10,0"/>
|
|
||||||
<ComboBox ItemsSource="{Binding SupportedMonitorTypes}" SelectedValue="{Binding SelectedMonitorType}" SelectedValuePath="Type" DisplayMemberPath="Name" Width="120" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="添加监控" Command="{Binding AddMonitorCommand}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 监控列表 -->
|
|
||||||
<DataGrid Grid.Row="3" ItemsSource="{Binding MonitoredTags}" AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="100"/>
|
|
||||||
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="150"/>
|
|
||||||
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="80"/>
|
|
||||||
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="*"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using Client.ViewModels;
|
|
||||||
using HandyControl.Controls;
|
|
||||||
|
|
||||||
namespace Client;
|
|
||||||
|
|
||||||
public partial class PlcWindow : GlowWindow
|
|
||||||
{
|
|
||||||
public PlcWindow(PlcViewModel viewModel)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Client.ProcessTestWindow"
|
|
||||||
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:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="Process / Python 测试工具" Height="700" Width="1000"
|
|
||||||
WindowStartupLocation="CenterOwner">
|
|
||||||
<hc:GlowWindow.Resources>
|
|
||||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
|
||||||
</hc:GlowWindow.Resources>
|
|
||||||
|
|
||||||
<Grid Margin="20">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="300"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- 主测试区域 -->
|
|
||||||
<TabControl Grid.Column="0" Margin="0,0,10,0">
|
|
||||||
<!-- Process 测试标签页 -->
|
|
||||||
<TabItem Header="Process 测试">
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel Margin="15">
|
|
||||||
<!-- 状态栏 -->
|
|
||||||
<Border Background="{DynamicResource PrimaryBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
|
||||||
<TextBlock Text="{Binding ProcessStatus}" Foreground="White" FontWeight="Bold"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 命令输入 -->
|
|
||||||
<GroupBox Header="命令配置" Margin="0,0,0,15">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Text="命令:" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding Command}" Margin="10,0,0,0"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Text="参数:" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
|
||||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Arguments}" Margin="10,10,0,0"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Text="超时(秒):" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
|
||||||
<hc:NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding TimeoutSeconds}"
|
|
||||||
Minimum="1" Maximum="600" Margin="10,10,0,0"/>
|
|
||||||
</Grid>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 快捷按钮 -->
|
|
||||||
<GroupBox Header="快速测试" Margin="0,0,0,15">
|
|
||||||
<WrapPanel Margin="10" Orientation="Horizontal">
|
|
||||||
<Button Content="执行命令" Command="{Binding RunProcessCommand}"
|
|
||||||
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
|
||||||
<Button Content="Shell 命令" Command="{Binding RunShellCommand}"
|
|
||||||
Margin="0,0,10,10" Padding="15,8"/>
|
|
||||||
<Button Content="测试 dotnet" Command="{Binding TestDotnetCommand}"
|
|
||||||
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
|
||||||
<Button Content="测试 Git" Command="{Binding TestGitCommand}"
|
|
||||||
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
|
||||||
</WrapPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 输出 -->
|
|
||||||
<GroupBox Header="执行输出">
|
|
||||||
<TextBox Text="{Binding ProcessOutput, Mode=OneWay}"
|
|
||||||
Height="300"
|
|
||||||
IsReadOnly="True"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
|
||||||
FontFamily="Consolas"
|
|
||||||
Margin="10"/>
|
|
||||||
</GroupBox>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<!-- Python 测试标签页 -->
|
|
||||||
<TabItem Header="Python 测试">
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel Margin="15">
|
|
||||||
<!-- Python 环境信息 -->
|
|
||||||
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Text="Python 环境" FontSize="16" FontWeight="Bold"/>
|
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<TextBlock Text="路径: " FontWeight="SemiBold"/>
|
|
||||||
<TextBlock Text="{Binding PythonPath}" Margin="5,0,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,5,0,0">
|
|
||||||
<TextBlock Text="版本: " FontWeight="SemiBold"/>
|
|
||||||
<TextBlock Text="{Binding PythonVersion}" Margin="5,0,20,0"/>
|
|
||||||
<Button Content="重新检测" Command="{Binding DetectPythonCommand}" Padding="10,4"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 状态栏 -->
|
|
||||||
<Border Background="{DynamicResource SuccessBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
|
||||||
<TextBlock Text="{Binding PythonStatus}" Foreground="White" FontWeight="Bold"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 代码执行 -->
|
|
||||||
<GroupBox Header="Python 代码执行" Margin="0,0,0,15">
|
|
||||||
<StackPanel Margin="10">
|
|
||||||
<TextBlock Text="Python 代码:" Margin="0,0,0,5"/>
|
|
||||||
<TextBox Text="{Binding PythonCode}"
|
|
||||||
Height="80"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
FontFamily="Consolas"
|
|
||||||
Margin="0,0,0,10"/>
|
|
||||||
<WrapPanel>
|
|
||||||
<Button Content="执行代码" Command="{Binding RunPythonCodeCommand}"
|
|
||||||
Margin="0,0,10,0" Padding="15,8" Style="{StaticResource ButtonSuccess}"/>
|
|
||||||
<Button Content="测试 JSON 输出" Command="{Binding TestPythonJsonCommand}"
|
|
||||||
Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
|
||||||
</WrapPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 脚本执行 -->
|
|
||||||
<GroupBox Header="Python 脚本执行">
|
|
||||||
<StackPanel Margin="10">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBox Text="{Binding PythonScriptPath}"
|
|
||||||
hc:InfoElement.Placeholder="脚本文件路径"/>
|
|
||||||
<Button Grid.Column="1" Content="创建测试脚本"
|
|
||||||
Command="{Binding CreateTestScriptCommand}"
|
|
||||||
Margin="5,0,0,0" Padding="10,4"/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="参数 (格式: key1=value1,key2=value2):" Margin="0,10,0,5"/>
|
|
||||||
<TextBox Text="{Binding PythonArgs}"
|
|
||||||
hc:InfoElement.Placeholder="例如: name=Sharp,count=3"
|
|
||||||
Margin="0,0,0,10"/>
|
|
||||||
|
|
||||||
<Button Content="执行脚本" Command="{Binding RunPythonScriptCommand}"
|
|
||||||
Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- Python 输出 -->
|
|
||||||
<GroupBox Header="Python 输出" Margin="0,15,0,0">
|
|
||||||
<TextBox Text="{Binding PythonOutput, Mode=OneWay}"
|
|
||||||
Height="200"
|
|
||||||
IsReadOnly="True"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
|
||||||
FontFamily="Consolas"
|
|
||||||
Margin="10"/>
|
|
||||||
</GroupBox>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
</TabControl>
|
|
||||||
|
|
||||||
<!-- 日志面板 -->
|
|
||||||
<Border Grid.Column="1" Background="{DynamicResource RegionBrush}"
|
|
||||||
Padding="15" CornerRadius="8">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Text="执行日志" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
|
|
||||||
|
|
||||||
<ListBox Grid.Row="1" ItemsSource="{Binding LogMessages}"
|
|
||||||
FontFamily="Consolas" FontSize="10"
|
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
|
||||||
<ListBox.ItemContainerStyle>
|
|
||||||
<Style TargetType="ListBoxItem">
|
|
||||||
<Setter Property="Padding" Value="2"/>
|
|
||||||
</Style>
|
|
||||||
</ListBox.ItemContainerStyle>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<Button Grid.Row="2" Content="清空日志" Command="{Binding ClearLogCommand}"
|
|
||||||
Margin="0,10,0,0" HorizontalAlignment="Stretch"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using Client.ViewModels;
|
|
||||||
|
|
||||||
namespace Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ProcessTestWindow.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class ProcessTestWindow
|
|
||||||
{
|
|
||||||
public ProcessTestWindow(ProcessTestViewModel viewModel)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Sharp.Client
|
|
||||||
|
|
||||||
基于 WPF 的工业客户端应用程序示例,展示了如何集成套件中的各个模块(PLC、串口、配置、日志、MVVM)。
|
|
||||||
|
|
||||||
## 功能概览
|
|
||||||
|
|
||||||
- **用户登录**:基于 `Sharp.Data` 的用户验证。
|
|
||||||
- **PLC 监控**:展示 `Plc.Siemens` 模块的使用,实时读取/写入 PLC 数据。
|
|
||||||
- **串口调试**:集成 `Ports` 模块,提供串口发现、连接与数据收发界面。
|
|
||||||
- **MVVM 架构**:使用 `Sharp.Wpf` 提供的 ViewModelBase、Command 和 DialogService。
|
|
||||||
|
|
||||||
## 关键集成点
|
|
||||||
|
|
||||||
### 1. 启动初始化 (App.xaml.cs)
|
|
||||||
展示了如何按顺序初始化基础设施:
|
|
||||||
- 加载配置 (`Sharp.Config`)
|
|
||||||
- 初始化日志 (`Sharp.Log`)
|
|
||||||
- 配置数据库 (`Sharp.Data`)
|
|
||||||
- 注册服务到 DI 容器 (`Sharp.Common.Global`)
|
|
||||||
|
|
||||||
### 2. PLC 交互 (PlcWindow)
|
|
||||||
- 演示了 `IPlcService` 的使用。
|
|
||||||
- 订阅模式(Subscribe) vs 即时读取(ReadAsync)。
|
|
||||||
- UI 绑定与数据转换。
|
|
||||||
|
|
||||||
### 3. 串口通信 (SerialPortWindow)
|
|
||||||
- 串口列表刷新与选择。
|
|
||||||
- 16进制与 ASCII 模式切换。
|
|
||||||
- 数据收发日志。
|
|
||||||
|
|
||||||
## 开发指南
|
|
||||||
|
|
||||||
1. 确保已安装 .NET 8 SDK。
|
|
||||||
2. 运行项目:
|
|
||||||
```bash
|
|
||||||
dotnet run --project Client.csproj
|
|
||||||
```
|
|
||||||
3. 默认登录账号(如使用示例数据库):`admin` / `123456`(需自行在初始化时播种)。
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
- `Hardware/`: 硬件抽象层(如扫码枪、打印机接口)。
|
|
||||||
- `Services/`: 客户端特定服务(如 `LoginService`)。
|
|
||||||
- `ViewModels/`: MVVM 视图模型。
|
|
||||||
- `Views/`: XAML 窗口与控件。
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Client.SerialPortWindow"
|
|
||||||
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:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="串口通信调试工具" Height="750" Width="1200"
|
|
||||||
WindowStartupLocation="CenterOwner">
|
|
||||||
|
|
||||||
<Grid Margin="20">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TabControl Grid.Row="0" Grid.RowSpan="2" Style="{StaticResource TabControlCapsule}">
|
|
||||||
<!-- Tab 1: 串口发现 -->
|
|
||||||
<TabItem Header="串口发现">
|
|
||||||
<Grid Margin="15">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Button Content="刷新串口列表" Click="RefreshPorts_Click" Style="{StaticResource ButtonPrimary}" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="仅显示USB串口" Click="ShowUsbPorts_Click" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="测试连接" Click="TestConnection_Click" Margin="0,0,10,0"/>
|
|
||||||
<TextBlock x:Name="PortCountText" Text="发现 0 个串口" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<DataGrid x:Name="PortsDataGrid" Grid.Row="1" AutoGenerateColumns="False" IsReadOnly="True"
|
|
||||||
SelectionMode="Single" HeadersVisibility="Column" CanUserResizeColumns="True">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="端口" Binding="{Binding PortName}" Width="100"/>
|
|
||||||
<DataGridTextColumn Header="描述" Binding="{Binding Description}" Width="200"/>
|
|
||||||
<DataGridTextColumn Header="制造商" Binding="{Binding Manufacturer}" Width="150"/>
|
|
||||||
<DataGridCheckBoxColumn Header="USB设备" Binding="{Binding IsUsbSerial}" Width="80"/>
|
|
||||||
<DataGridTextColumn Header="VID" Binding="{Binding VendorId}" Width="80"/>
|
|
||||||
<DataGridTextColumn Header="PID" Binding="{Binding ProductId}" Width="80"/>
|
|
||||||
<DataGridCheckBoxColumn Header="被占用" Binding="{Binding IsInUse}" Width="80"/>
|
|
||||||
<DataGridTextColumn Header="设备ID" Binding="{Binding DeviceId}" Width="*"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" x:Name="DiscoveryStatusText" Text="点击'刷新串口列表'开始"
|
|
||||||
Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,10,0,0"/>
|
|
||||||
</Grid>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<!-- Tab 2: 基础通信 -->
|
|
||||||
<TabItem Header="基础通信">
|
|
||||||
<Grid Margin="15">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 连接配置 -->
|
|
||||||
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="PortNameCombo" Width="100" Margin="0,0,15,0"/>
|
|
||||||
|
|
||||||
<TextBlock Text="波特率:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="BaudRateCombo" Width="100" Margin="0,0,15,0" SelectedIndex="3">
|
|
||||||
<ComboBoxItem Content="4800"/>
|
|
||||||
<ComboBoxItem Content="9600"/>
|
|
||||||
<ComboBoxItem Content="19200"/>
|
|
||||||
<ComboBoxItem Content="38400"/>
|
|
||||||
<ComboBoxItem Content="57600"/>
|
|
||||||
<ComboBoxItem Content="115200"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<TextBlock Text="数据位:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="DataBitsCombo" Width="60" Margin="0,0,15,0" SelectedIndex="3">
|
|
||||||
<ComboBoxItem Content="5"/>
|
|
||||||
<ComboBoxItem Content="6"/>
|
|
||||||
<ComboBoxItem Content="7"/>
|
|
||||||
<ComboBoxItem Content="8"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<TextBlock Text="校验位:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="ParityCombo" Width="80" Margin="0,0,15,0" SelectedIndex="0">
|
|
||||||
<ComboBoxItem Content="None"/>
|
|
||||||
<ComboBoxItem Content="Odd"/>
|
|
||||||
<ComboBoxItem Content="Even"/>
|
|
||||||
<ComboBoxItem Content="Mark"/>
|
|
||||||
<ComboBoxItem Content="Space"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<TextBlock Text="停止位:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="StopBitsCombo" Width="80" Margin="0,0,0,0" SelectedIndex="1">
|
|
||||||
<ComboBoxItem Content="None"/>
|
|
||||||
<ComboBoxItem Content="One"/>
|
|
||||||
<ComboBoxItem Content="Two"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<Button x:Name="OpenButton" Content="打开串口" Click="OpenPort_Click" Style="{StaticResource ButtonSuccess}" Margin="0,0,10,0"/>
|
|
||||||
<Button x:Name="CloseButton" Content="关闭串口" Click="ClosePort_Click" Style="{StaticResource ButtonDanger}" Margin="0,0,10,0" IsEnabled="False"/>
|
|
||||||
<TextBlock x:Name="ConnectionStatusText" Text="未连接" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 发送区域 -->
|
|
||||||
<GroupBox Header="发送数据" Grid.Row="1" Margin="0,0,0,15">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBox x:Name="SendDataTextBox" Width="400" hc:InfoElement.Placeholder="输入要发送的数据 (十六进制,如: 01 02 03)" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="发送 (HEX)" Click="SendHex_Click" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="发送 (文本)" Click="SendText_Click" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="清空" Click="ClearSend_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<CheckBox x:Name="AutoAddNewLineCheckBox" Content="自动添加换行符" IsChecked="True" Margin="0,0,15,0"/>
|
|
||||||
<TextBlock Text="编码:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="EncodingCombo" Width="100" SelectedIndex="0">
|
|
||||||
<ComboBoxItem Content="UTF-8"/>
|
|
||||||
<ComboBoxItem Content="ASCII"/>
|
|
||||||
<ComboBoxItem Content="GBK"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 接收区域 -->
|
|
||||||
<GroupBox Header="接收数据" Grid.Row="2" Margin="0,0,0,10">
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="10">
|
|
||||||
<CheckBox x:Name="HexDisplayCheckBox" Content="HEX显示" IsChecked="True" Margin="0,0,15,0"/>
|
|
||||||
<CheckBox x:Name="AutoScrollCheckBox" Content="自动滚动" IsChecked="True" Margin="0,0,15,0"/>
|
|
||||||
<Button Content="清空接收区" Click="ClearReceive_Click" Margin="0,0,10,0"/>
|
|
||||||
<TextBlock x:Name="ReceivedBytesText" Text="已接收: 0 字节" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<TextBox x:Name="ReceiveTextBox" Grid.Row="3" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
|
|
||||||
TextWrapping="Wrap" FontFamily="Consolas" Background="{DynamicResource RegionBrush}"/>
|
|
||||||
</Grid>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<!-- Tab 3: 高级监听 -->
|
|
||||||
<TabItem Header="高级监听">
|
|
||||||
<Grid Margin="15">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
|
||||||
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="ServicePortCombo" Width="100" Margin="0,0,15,0"/>
|
|
||||||
<TextBlock Text="波特率:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="ServiceBaudRateCombo" Width="100" Margin="0,0,15,0" SelectedIndex="3">
|
|
||||||
<ComboBoxItem Content="9600"/>
|
|
||||||
<ComboBoxItem Content="19200"/>
|
|
||||||
<ComboBoxItem Content="38400"/>
|
|
||||||
<ComboBoxItem Content="57600"/>
|
|
||||||
<ComboBoxItem Content="115200"/>
|
|
||||||
</ComboBox>
|
|
||||||
<CheckBox x:Name="AutoReconnectCheckBox" Content="自动重连" IsChecked="True" Margin="0,0,15,0" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Button x:Name="StartListeningButton" Content="启动监听" Click="StartListening_Click" Style="{StaticResource ButtonSuccess}" Margin="0,0,10,0"/>
|
|
||||||
<Button x:Name="StopListeningButton" Content="停止监听" Click="StopListening_Click" Style="{StaticResource ButtonDanger}" Margin="0,0,10,0" IsEnabled="False"/>
|
|
||||||
<TextBlock x:Name="ServiceStatusText" Text="未启动" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<DataGrid x:Name="EventLogDataGrid" Grid.Row="1" AutoGenerateColumns="False" IsReadOnly="True"
|
|
||||||
HeadersVisibility="Column" CanUserResizeColumns="True" Margin="0,0,0,15">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="时间" Binding="{Binding Time, StringFormat=HH:mm:ss.fff}" Width="100"/>
|
|
||||||
<DataGridTextColumn Header="类型" Binding="{Binding EventType}" Width="100"/>
|
|
||||||
<DataGridTextColumn Header="数据/消息" Binding="{Binding Data}" Width="*"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="2" Orientation="Horizontal">
|
|
||||||
<Button Content="清空日志" Click="ClearEventLog_Click" Margin="0,0,10,0"/>
|
|
||||||
<TextBlock x:Name="EventCountText" Text="事件数: 0" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<!-- Tab 4: 数据工具 -->
|
|
||||||
<TabItem Header="数据工具">
|
|
||||||
<Grid Margin="15">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 数据转换 -->
|
|
||||||
<GroupBox Header="数据转换" Margin="0,0,0,15">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBox x:Name="ConvertInputTextBox" Width="300" hc:InfoElement.Placeholder="输入数据" Margin="0,0,10,0"/>
|
|
||||||
<ComboBox x:Name="ConvertTypeCombo" Width="120" Margin="0,0,10,0" SelectedIndex="0">
|
|
||||||
<ComboBoxItem Content="HEX -> 字节"/>
|
|
||||||
<ComboBoxItem Content="字节 -> HEX"/>
|
|
||||||
<ComboBoxItem Content="字节 -> Int16"/>
|
|
||||||
<ComboBoxItem Content="字节 -> Int32"/>
|
|
||||||
<ComboBoxItem Content="字节 -> Float"/>
|
|
||||||
<ComboBoxItem Content="Int32 -> 字节"/>
|
|
||||||
</ComboBox>
|
|
||||||
<CheckBox x:Name="BigEndianCheckBox" Content="大端序" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
|
||||||
<Button Content="转换" Click="ConvertData_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Text="转换结果:" Margin="0,10,0,5"/>
|
|
||||||
<TextBox Grid.Row="2" x:Name="ConvertResultTextBox" IsReadOnly="True" Background="{DynamicResource RegionBrush}"/>
|
|
||||||
</Grid>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 校验计算 -->
|
|
||||||
<GroupBox Header="校验计算" Grid.Row="1" Margin="0,0,0,15">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBox x:Name="ChecksumInputTextBox" Width="400" hc:InfoElement.Placeholder="输入十六进制数据 (如: 01 02 03)" Margin="0,0,10,0"/>
|
|
||||||
<Button Content="CRC16" Click="CalculateCrc16_Click" Margin="0,0,5,0"/>
|
|
||||||
<Button Content="LRC" Click="CalculateLrc_Click" Margin="0,0,5,0"/>
|
|
||||||
<Button Content="XOR" Click="CalculateXor_Click" Margin="0,0,5,0"/>
|
|
||||||
<Button Content="累加和" Click="CalculateChecksum_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<TextBlock Text="校验结果:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
|
||||||
<TextBox x:Name="ChecksumResultTextBox" Width="200" IsReadOnly="True" Background="{DynamicResource RegionBrush}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 诊断工具 -->
|
|
||||||
<GroupBox Header="诊断工具" Grid.Row="2" Margin="0,0,0,15">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
|
||||||
<ComboBox x:Name="DiagPortCombo" Width="100" Margin="0,0,10,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
|
||||||
<Button Content="测试连接" Click="DiagTestConnection_Click" Margin="0,0,5,0"/>
|
|
||||||
<Button Content="波特率扫描" Click="ScanBaudRates_Click" Margin="0,0,5,0"/>
|
|
||||||
<Button Content="流量监控(10s)" Click="MonitorTraffic_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<!-- 诊断结果 -->
|
|
||||||
<TextBox x:Name="DiagResultTextBox" Grid.Row="3" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
|
|
||||||
TextWrapping="Wrap" FontFamily="Consolas" Background="{DynamicResource RegionBrush}"/>
|
|
||||||
</Grid>
|
|
||||||
</TabItem>
|
|
||||||
</TabControl>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,644 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.IO.Ports;
|
|
||||||
using System.Text;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using HandyControl.Controls;
|
|
||||||
using Ports.Abstractions;
|
|
||||||
using Ports.Discovery;
|
|
||||||
using Ports.Drivers;
|
|
||||||
using Ports.Models;
|
|
||||||
using Ports.Services;
|
|
||||||
using Ports.Utils;
|
|
||||||
|
|
||||||
namespace Client;
|
|
||||||
|
|
||||||
public partial class SerialPortWindow
|
|
||||||
{
|
|
||||||
private ISerialPortDriver? _driver;
|
|
||||||
private ISerialPortService? _service;
|
|
||||||
private int _totalReceivedBytes;
|
|
||||||
private readonly ObservableCollection<EventLogItem> _eventLog = new();
|
|
||||||
|
|
||||||
public SerialPortWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
InitializeComboBoxes();
|
|
||||||
EventLogDataGrid.ItemsSource = _eventLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComboBoxes()
|
|
||||||
{
|
|
||||||
// 初始化波特率下拉框
|
|
||||||
BaudRateCombo.SelectedItem = BaudRateCombo.Items[4]; // 115200
|
|
||||||
|
|
||||||
// 加载串口列表
|
|
||||||
RefreshPortsList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshPortsList()
|
|
||||||
{
|
|
||||||
var result = SerialPortDiscovery.GetAvailablePorts();
|
|
||||||
if (result.IsSuccess && result.Value != null)
|
|
||||||
{
|
|
||||||
var portNames = result.Value.Select(p => p.PortName).ToList();
|
|
||||||
|
|
||||||
PortNameCombo.ItemsSource = portNames;
|
|
||||||
ServicePortCombo.ItemsSource = portNames;
|
|
||||||
DiagPortCombo.ItemsSource = portNames;
|
|
||||||
|
|
||||||
if (portNames.Count > 0)
|
|
||||||
{
|
|
||||||
PortNameCombo.SelectedIndex = 0;
|
|
||||||
ServicePortCombo.SelectedIndex = 0;
|
|
||||||
DiagPortCombo.SelectedIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Tab 1: 串口发现
|
|
||||||
|
|
||||||
private void RefreshPorts_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var result = SerialPortDiscovery.GetAvailablePorts();
|
|
||||||
if (result.IsSuccess && result.Value != null)
|
|
||||||
{
|
|
||||||
PortsDataGrid.ItemsSource = result.Value;
|
|
||||||
PortCountText.Text = $"发现 {result.Value.Count} 个串口";
|
|
||||||
DiscoveryStatusText.Text = $"最后刷新: {DateTime.Now:HH:mm:ss}";
|
|
||||||
RefreshPortsList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DiscoveryStatusText.Text = $"错误: {result.ErrorMessage}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowUsbPorts_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var result = SerialPortDiscovery.GetUsbSerialPorts();
|
|
||||||
if (result.IsSuccess && result.Value != null)
|
|
||||||
{
|
|
||||||
PortsDataGrid.ItemsSource = result.Value;
|
|
||||||
PortCountText.Text = $"发现 {result.Value.Count} 个USB串口";
|
|
||||||
DiscoveryStatusText.Text = $"仅显示USB设备";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DiscoveryStatusText.Text = $"错误: {result.ErrorMessage}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void TestConnection_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var selected = PortsDataGrid.SelectedItem as SerialPortInfo;
|
|
||||||
if (selected == null)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请先选择一个串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DiscoveryStatusText.Text = $"正在测试 {selected.PortName}...";
|
|
||||||
|
|
||||||
var options = new SerialPortOptions { PortName = selected.PortName };
|
|
||||||
var result = await SerialPortDiagnostics.TestConnectionAsync(selected.PortName, options);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
DiscoveryStatusText.Text = "测试完成";
|
|
||||||
HandyControl.Controls.MessageBox.Show(result.Value, "测试结果");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DiscoveryStatusText.Text = $"测试失败: {result.ErrorMessage}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Tab 2: 基础通信
|
|
||||||
|
|
||||||
private async void OpenPort_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (PortNameCombo.SelectedItem == null)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new SerialPortOptions
|
|
||||||
{
|
|
||||||
PortName = PortNameCombo.SelectedItem.ToString()!,
|
|
||||||
BaudRate = int.Parse(((ComboBoxItem)BaudRateCombo.SelectedItem).Content.ToString()!),
|
|
||||||
DataBits = int.Parse(((ComboBoxItem)DataBitsCombo.SelectedItem).Content.ToString()!),
|
|
||||||
Parity = Enum.Parse<Parity>(((ComboBoxItem)ParityCombo.SelectedItem).Content.ToString()!),
|
|
||||||
StopBits = Enum.Parse<StopBits>(((ComboBoxItem)StopBitsCombo.SelectedItem).Content.ToString()!)
|
|
||||||
};
|
|
||||||
|
|
||||||
_driver = new SerialPortDriver();
|
|
||||||
var result = await _driver.OpenAsync(options);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
ConnectionStatusText.Text = "已连接";
|
|
||||||
OpenButton.IsEnabled = false;
|
|
||||||
CloseButton.IsEnabled = true;
|
|
||||||
_totalReceivedBytes = 0;
|
|
||||||
|
|
||||||
// 启动接收线程
|
|
||||||
_ = Task.Run(ReceiveLoop);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ConnectionStatusText.Text = $"连接失败: {result.ErrorMessage}";
|
|
||||||
HandyControl.Controls.MessageBox.Show(result.ErrorMessage ?? "连接失败", "错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ClosePort_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_driver != null)
|
|
||||||
{
|
|
||||||
await _driver.CloseAsync();
|
|
||||||
_driver.Dispose();
|
|
||||||
_driver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionStatusText.Text = "未连接";
|
|
||||||
OpenButton.IsEnabled = true;
|
|
||||||
CloseButton.IsEnabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void SendHex_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_driver == null || !_driver.IsOpen)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请先打开串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hexResult = SerialDataConverter.HexStringToBytes(SendDataTextBox.Text);
|
|
||||||
if (hexResult.IsFailure)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show(hexResult.ErrorMessage ?? "数据格式错误", "错误");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _driver.WriteBytesAsync(hexResult.Value!);
|
|
||||||
if (result.IsFailure)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show(result.ErrorMessage ?? "发送失败", "错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void SendText_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_driver == null || !_driver.IsOpen)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请先打开串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoding = GetSelectedEncoding();
|
|
||||||
var text = SendDataTextBox.Text;
|
|
||||||
|
|
||||||
if (AutoAddNewLineCheckBox.IsChecked == true)
|
|
||||||
text += "\r\n";
|
|
||||||
|
|
||||||
var bytesResult = SerialDataConverter.StringToBytes(text, encoding);
|
|
||||||
if (bytesResult.IsSuccess && bytesResult.Value != null)
|
|
||||||
{
|
|
||||||
await _driver.WriteBytesAsync(bytesResult.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearSend_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
SendDataTextBox.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearReceive_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
ReceiveTextBox.Clear();
|
|
||||||
_totalReceivedBytes = 0;
|
|
||||||
ReceivedBytesText.Text = "已接收: 0 字节";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReceiveLoop()
|
|
||||||
{
|
|
||||||
while (_driver != null && _driver.IsOpen)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_driver.BytesToRead > 0)
|
|
||||||
{
|
|
||||||
var result = await _driver.ReadAvailableAsync();
|
|
||||||
if (result.IsSuccess && result.Value != null && result.Value.Length > 0)
|
|
||||||
{
|
|
||||||
_totalReceivedBytes += result.Value.Length;
|
|
||||||
Dispatcher.Invoke(() => DisplayReceivedData(result.Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(10);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisplayReceivedData(byte[] data)
|
|
||||||
{
|
|
||||||
string display;
|
|
||||||
if (HexDisplayCheckBox.IsChecked == true)
|
|
||||||
{
|
|
||||||
var hexResult = SerialDataConverter.BytesToHexString(data, " ");
|
|
||||||
display = hexResult.Value + "\r\n";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var encoding = GetSelectedEncoding();
|
|
||||||
var strResult = SerialDataConverter.BytesToString(data, encoding);
|
|
||||||
display = strResult.Value ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReceiveTextBox.AppendText($"[{DateTime.Now:HH:mm:ss}] {display}");
|
|
||||||
ReceivedBytesText.Text = $"已接收: {_totalReceivedBytes} 字节";
|
|
||||||
|
|
||||||
if (AutoScrollCheckBox.IsChecked == true)
|
|
||||||
{
|
|
||||||
ReceiveTextBox.ScrollToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Tab 3: 高级监听
|
|
||||||
|
|
||||||
private async void StartListening_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (ServicePortCombo.SelectedItem == null)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new SerialPortOptions
|
|
||||||
{
|
|
||||||
PortName = ServicePortCombo.SelectedItem.ToString()!,
|
|
||||||
BaudRate = int.Parse(((ComboBoxItem)ServiceBaudRateCombo.SelectedItem).Content.ToString()!)
|
|
||||||
};
|
|
||||||
|
|
||||||
_service = new SerialPortService(new SerialPortDriver());
|
|
||||||
|
|
||||||
// 订阅事件
|
|
||||||
_service.DataReceived += OnServiceDataReceived;
|
|
||||||
_service.ErrorOccurred += OnServiceErrorOccurred;
|
|
||||||
_service.ConnectionStateChanged += OnServiceConnectionStateChanged;
|
|
||||||
|
|
||||||
var autoReconnect = AutoReconnectCheckBox.IsChecked == true;
|
|
||||||
var result = await _service.StartListeningAsync(options, autoReconnect);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
ServiceStatusText.Text = "监听中";
|
|
||||||
StartListeningButton.IsEnabled = false;
|
|
||||||
StopListeningButton.IsEnabled = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ServiceStatusText.Text = $"启动失败";
|
|
||||||
HandyControl.Controls.MessageBox.Show(result.ErrorMessage ?? "启动失败", "错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void StopListening_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_service != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _service.StopListeningAsync();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// 任务取消是正常情况,忽略
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show($"停止监听时发生错误: {ex.Message}", "错误");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_service.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// 忽略清理异常
|
|
||||||
}
|
|
||||||
|
|
||||||
_service = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServiceStatusText.Text = "未启动";
|
|
||||||
StartListeningButton.IsEnabled = true;
|
|
||||||
StopListeningButton.IsEnabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnServiceDataReceived(object? sender, DataReceivedEventArgs e)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
var hexResult = SerialDataConverter.BytesToHexString(e.Data, " ");
|
|
||||||
_eventLog.Add(new EventLogItem
|
|
||||||
{
|
|
||||||
Time = e.ReceivedAt,
|
|
||||||
EventType = "数据接收",
|
|
||||||
Data = $"{e.Data.Length} 字节: {hexResult.Value}"
|
|
||||||
});
|
|
||||||
EventCountText.Text = $"事件数: {_eventLog.Count}";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnServiceErrorOccurred(object? sender, Ports.Models.ErrorEventArgs e)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_eventLog.Add(new EventLogItem
|
|
||||||
{
|
|
||||||
Time = e.OccurredAt,
|
|
||||||
EventType = "错误",
|
|
||||||
Data = e.ErrorMessage
|
|
||||||
});
|
|
||||||
EventCountText.Text = $"事件数: {_eventLog.Count}";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnServiceConnectionStateChanged(object? sender, ConnectionStateChangedEventArgs e)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_eventLog.Add(new EventLogItem
|
|
||||||
{
|
|
||||||
Time = e.ChangedAt,
|
|
||||||
EventType = "状态变化",
|
|
||||||
Data = $"{e.PreviousState} -> {e.CurrentState}"
|
|
||||||
});
|
|
||||||
ServiceStatusText.Text = e.CurrentState.ToString();
|
|
||||||
EventCountText.Text = $"事件数: {_eventLog.Count}";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearEventLog_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
_eventLog.Clear();
|
|
||||||
EventCountText.Text = "事件数: 0";
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Tab 4: 数据工具
|
|
||||||
|
|
||||||
private void ConvertData_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var input = ConvertInputTextBox.Text;
|
|
||||||
var type = ((ComboBoxItem)ConvertTypeCombo.SelectedItem).Content.ToString();
|
|
||||||
var isBigEndian = BigEndianCheckBox.IsChecked == true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case "HEX -> 字节":
|
|
||||||
var hexBytes = SerialDataConverter.HexStringToBytes(input);
|
|
||||||
ConvertResultTextBox.Text = hexBytes.IsSuccess ? $"字节数组长度: {hexBytes.Value!.Length}" : hexBytes.ErrorMessage;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "字节 -> HEX":
|
|
||||||
var bytes = SerialDataConverter.HexStringToBytes(input);
|
|
||||||
if (bytes.IsSuccess)
|
|
||||||
{
|
|
||||||
var hex = SerialDataConverter.BytesToHexString(bytes.Value!, " ");
|
|
||||||
ConvertResultTextBox.Text = hex.Value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "字节 -> Int16":
|
|
||||||
var int16Bytes = SerialDataConverter.HexStringToBytes(input);
|
|
||||||
if (int16Bytes.IsSuccess)
|
|
||||||
{
|
|
||||||
var int16 = SerialDataConverter.BytesToInt16(int16Bytes.Value!, isBigEndian);
|
|
||||||
ConvertResultTextBox.Text = int16.IsSuccess ? int16.Value.ToString() : int16.ErrorMessage;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "字节 -> Int32":
|
|
||||||
var int32Bytes = SerialDataConverter.HexStringToBytes(input);
|
|
||||||
if (int32Bytes.IsSuccess)
|
|
||||||
{
|
|
||||||
var int32 = SerialDataConverter.BytesToInt32(int32Bytes.Value!, isBigEndian);
|
|
||||||
ConvertResultTextBox.Text = int32.IsSuccess ? int32.Value.ToString() : int32.ErrorMessage;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "字节 -> Float":
|
|
||||||
var floatBytes = SerialDataConverter.HexStringToBytes(input);
|
|
||||||
if (floatBytes.IsSuccess)
|
|
||||||
{
|
|
||||||
var floatValue = SerialDataConverter.BytesToFloat(floatBytes.Value!, isBigEndian);
|
|
||||||
ConvertResultTextBox.Text = floatValue.IsSuccess ? floatValue.Value.ToString("F4") : floatValue.ErrorMessage;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Int32 -> 字节":
|
|
||||||
if (int.TryParse(input, out var intValue))
|
|
||||||
{
|
|
||||||
var intBytes = SerialDataConverter.Int32ToBytes(intValue, isBigEndian);
|
|
||||||
if (intBytes.IsSuccess)
|
|
||||||
{
|
|
||||||
var hex = SerialDataConverter.BytesToHexString(intBytes.Value!, " ");
|
|
||||||
ConvertResultTextBox.Text = hex.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ConvertResultTextBox.Text = "输入不是有效的整数";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ConvertResultTextBox.Text = $"转换错误: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CalculateCrc16_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
|
||||||
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
|
||||||
{
|
|
||||||
var crc = SerialChecksumHelper.CalculateCrc16(hexBytes.Value);
|
|
||||||
ChecksumResultTextBox.Text = crc.IsSuccess ? $"0x{crc.Value:X4}" : crc.ErrorMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CalculateLrc_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
|
||||||
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
|
||||||
{
|
|
||||||
var lrc = SerialChecksumHelper.CalculateLrc(hexBytes.Value);
|
|
||||||
ChecksumResultTextBox.Text = lrc.IsSuccess ? $"0x{lrc.Value:X2}" : lrc.ErrorMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CalculateXor_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
|
||||||
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
|
||||||
{
|
|
||||||
var xor = SerialChecksumHelper.CalculateXor(hexBytes.Value);
|
|
||||||
ChecksumResultTextBox.Text = xor.IsSuccess ? $"0x{xor.Value:X2}" : xor.ErrorMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CalculateChecksum_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
|
||||||
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
|
||||||
{
|
|
||||||
var checksum = SerialChecksumHelper.CalculateChecksum(hexBytes.Value);
|
|
||||||
ChecksumResultTextBox.Text = checksum.IsSuccess ? $"0x{checksum.Value:X2}" : checksum.ErrorMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void DiagTestConnection_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DiagPortCombo.SelectedItem == null)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DiagResultTextBox.Text = "正在测试连接...\r\n";
|
|
||||||
|
|
||||||
var portName = DiagPortCombo.SelectedItem.ToString()!;
|
|
||||||
var options = new SerialPortOptions { PortName = portName };
|
|
||||||
|
|
||||||
var result = await SerialPortDiagnostics.TestConnectionAsync(portName, options);
|
|
||||||
DiagResultTextBox.AppendText(result.IsSuccess ? result.Value! : $"错误: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ScanBaudRates_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DiagPortCombo.SelectedItem == null)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DiagResultTextBox.Text = "正在扫描波特率...\r\n";
|
|
||||||
|
|
||||||
var portName = DiagPortCombo.SelectedItem.ToString()!;
|
|
||||||
var result = await SerialPortDiagnostics.TestBaudRatesAsync(
|
|
||||||
portName, 4800, 9600, 19200, 38400, 57600, 115200);
|
|
||||||
|
|
||||||
if (result.IsSuccess && result.Value != null)
|
|
||||||
{
|
|
||||||
DiagResultTextBox.AppendText("\r\n波特率测试结果:\r\n");
|
|
||||||
foreach (var kvp in result.Value)
|
|
||||||
{
|
|
||||||
DiagResultTextBox.AppendText($" {kvp.Key,6} bps: {(kvp.Value ? "✓ 成功" : "✗ 失败")}\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DiagResultTextBox.AppendText($"错误: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void MonitorTraffic_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DiagPortCombo.SelectedItem == null)
|
|
||||||
{
|
|
||||||
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DiagResultTextBox.Text = "正在监控流量 (10秒)...\r\n";
|
|
||||||
|
|
||||||
var portName = DiagPortCombo.SelectedItem.ToString()!;
|
|
||||||
var options = new SerialPortOptions { PortName = portName };
|
|
||||||
|
|
||||||
var result = await SerialPortDiagnostics.MonitorTrafficAsync(
|
|
||||||
portName,
|
|
||||||
options,
|
|
||||||
(data, isSent) => Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
var hexResult = SerialDataConverter.BytesToHexString(data, " ");
|
|
||||||
DiagResultTextBox.AppendText($"[{DateTime.Now:HH:mm:ss}] {(isSent ? "发送" : "接收")}: {hexResult.Value}\r\n");
|
|
||||||
}),
|
|
||||||
TimeSpan.FromSeconds(10));
|
|
||||||
|
|
||||||
if (result.IsSuccess && result.Value != null)
|
|
||||||
{
|
|
||||||
DiagResultTextBox.AppendText($"\r\n{result.Value}\r\n");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DiagResultTextBox.AppendText($"错误: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 辅助方法
|
|
||||||
|
|
||||||
private Encoding GetSelectedEncoding()
|
|
||||||
{
|
|
||||||
var encoding = ((ComboBoxItem)EncodingCombo.SelectedItem)?.Content.ToString();
|
|
||||||
return encoding switch
|
|
||||||
{
|
|
||||||
"ASCII" => Encoding.ASCII,
|
|
||||||
"GBK" => Encoding.GetEncoding("GBK"),
|
|
||||||
_ => Encoding.UTF8
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnClosed(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnClosed(e);
|
|
||||||
|
|
||||||
// 清理资源
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_driver?.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// 忽略清理异常
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_service?.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// 忽略清理异常
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EventLogItem
|
|
||||||
{
|
|
||||||
public DateTime Time { get; set; }
|
|
||||||
public string EventType { get; set; } = string.Empty;
|
|
||||||
public string Data { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# 串口通信调试工具使用说明
|
|
||||||
|
|
||||||
## 功能概览
|
|
||||||
|
|
||||||
串口通信调试工具集成了Ports模块的所有功能,分为4个主要标签页:
|
|
||||||
|
|
||||||
## Tab 1: 串口发现
|
|
||||||
|
|
||||||
**功能:**
|
|
||||||
- 发现和查询所有可用串口
|
|
||||||
- 识别USB转串口设备
|
|
||||||
- 查看串口详细信息(描述、厂商、VID/PID)
|
|
||||||
- 检查串口是否被占用
|
|
||||||
- 测试串口连接
|
|
||||||
|
|
||||||
**操作步骤:**
|
|
||||||
1. 点击"刷新串口列表"获取所有串口
|
|
||||||
2. 点击"仅显示USB串口"过滤USB设备
|
|
||||||
3. 选择一个串口,点击"测试连接"进行连接测试
|
|
||||||
|
|
||||||
## Tab 2: 基础通信
|
|
||||||
|
|
||||||
**功能:**
|
|
||||||
- 配置串口参数(波特率、数据位、校验位、停止位)
|
|
||||||
- 打开/关闭串口连接
|
|
||||||
- 发送数据(十六进制或文本)
|
|
||||||
- 实时接收和显示数据
|
|
||||||
- 支持多种字符编码(UTF-8、ASCII、GBK)
|
|
||||||
|
|
||||||
**操作步骤:**
|
|
||||||
1. 选择串口和配置参数
|
|
||||||
2. 点击"打开串口"
|
|
||||||
3. 在发送框输入数据
|
|
||||||
- 十六进制格式:01 02 03 04
|
|
||||||
- 文本格式:直接输入文本
|
|
||||||
4. 点击"发送 (HEX)"或"发送 (文本)"
|
|
||||||
5. 接收区会实时显示收到的数据
|
|
||||||
|
|
||||||
**提示:**
|
|
||||||
- 勾选"自动添加换行符"会在文本末尾添加\r\n
|
|
||||||
- 勾选"HEX显示"会以十六进制格式显示接收数据
|
|
||||||
- 勾选"自动滚动"会自动滚动到最新数据
|
|
||||||
|
|
||||||
## Tab 3: 高级监听
|
|
||||||
|
|
||||||
**功能:**
|
|
||||||
- 使用服务层进行高级数据监听
|
|
||||||
- 自动重连机制
|
|
||||||
- 事件日志记录(数据接收、错误、状态变化)
|
|
||||||
- 实时监控连接状态
|
|
||||||
|
|
||||||
**操作步骤:**
|
|
||||||
1. 选择串口和波特率
|
|
||||||
2. 勾选"自动重连"启用断线重连
|
|
||||||
3. 点击"启动监听"
|
|
||||||
4. 事件日志会实时显示所有事件
|
|
||||||
5. 点击"停止监听"结束
|
|
||||||
|
|
||||||
**事件类型:**
|
|
||||||
- 数据接收:显示接收到的字节数和内容
|
|
||||||
- 错误:显示错误信息
|
|
||||||
- 状态变化:显示连接状态变化(连接中→已连接→断开等)
|
|
||||||
|
|
||||||
## Tab 4: 数据工具
|
|
||||||
|
|
||||||
### 数据转换
|
|
||||||
**支持的转换:**
|
|
||||||
- HEX ↔ 字节数组
|
|
||||||
- 字节 → Int16/Int32/Float
|
|
||||||
- Int32 → 字节
|
|
||||||
- 大端/小端序切换
|
|
||||||
|
|
||||||
**操作:**
|
|
||||||
1. 输入数据
|
|
||||||
2. 选择转换类型
|
|
||||||
3. 勾选"大端序"(如需要)
|
|
||||||
4. 点击"转换"
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
- 输入:`01 02 03 04`,选择"字节 → Int32",大端序 → 结果:16909060
|
|
||||||
|
|
||||||
### 校验计算
|
|
||||||
**支持的校验算法:**
|
|
||||||
- CRC16(Modbus标准)
|
|
||||||
- LRC(纵向冗余校验)
|
|
||||||
- XOR(异或校验)
|
|
||||||
- 累加和
|
|
||||||
|
|
||||||
**操作:**
|
|
||||||
1. 输入十六进制数据(如:01 02 03)
|
|
||||||
2. 点击对应的校验算法按钮
|
|
||||||
3. 查看校验结果
|
|
||||||
|
|
||||||
### 诊断工具
|
|
||||||
**功能:**
|
|
||||||
- 测试连接:测试指定串口的连接性
|
|
||||||
- 波特率扫描:自动测试多个波特率
|
|
||||||
- 流量监控:监控10秒内的数据收发
|
|
||||||
|
|
||||||
**操作:**
|
|
||||||
1. 选择要诊断的串口
|
|
||||||
2. 点击相应功能按钮
|
|
||||||
3. 在结果区查看诊断报告
|
|
||||||
|
|
||||||
## 使用技巧
|
|
||||||
|
|
||||||
1. **串口被占用?**
|
|
||||||
- 在"串口发现"页查看哪个串口被占用
|
|
||||||
- 确保其他程序没有打开该串口
|
|
||||||
|
|
||||||
2. **数据接收不完整?**
|
|
||||||
- 检查波特率设置是否正确
|
|
||||||
- 尝试使用"基础通信"的实时接收
|
|
||||||
|
|
||||||
3. **需要自动重连?**
|
|
||||||
- 使用"高级监听"标签页
|
|
||||||
- 勾选"自动重连"选项
|
|
||||||
|
|
||||||
4. **调试Modbus设备?**
|
|
||||||
- 使用"数据工具"计算CRC16
|
|
||||||
- 使用"基础通信"发送命令并查看响应
|
|
||||||
|
|
||||||
5. **不确定波特率?**
|
|
||||||
- 使用"诊断工具"的波特率扫描功能
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
**Q: 打开串口失败?**
|
|
||||||
A: 检查串口是否被其他程序占用,或者没有权限访问该串口
|
|
||||||
|
|
||||||
**Q: 发送十六进制数据格式?**
|
|
||||||
A: 使用空格分隔,如:01 02 03 04,也支持不带空格:01020304
|
|
||||||
|
|
||||||
**Q: 如何保存接收的数据?**
|
|
||||||
A: 可以从接收框复制文本,后续版本会增加保存功能
|
|
||||||
|
|
||||||
**Q: 支持哪些操作系统?**
|
|
||||||
A: Windows(完整功能)、Linux、macOS(基础功能)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Client.Services;
|
|
||||||
|
|
||||||
public interface IAuthenticationService
|
|
||||||
{
|
|
||||||
Task<AuthenticationResult> LoginAsync(string userName, string password, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record AuthenticationResult(bool Succeeded, string? Error, string? DisplayName);
|
|
||||||
|
|
||||||
public sealed class DemoAuthenticationService : IAuthenticationService
|
|
||||||
{
|
|
||||||
public async Task<AuthenticationResult> LoginAsync(string userName, string password, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await Task.Delay(600, cancellationToken);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password))
|
|
||||||
{
|
|
||||||
return new AuthenticationResult(false, "请输入账号和密码", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(userName, "admin", StringComparison.OrdinalIgnoreCase) || password != "123456")
|
|
||||||
{
|
|
||||||
return new AuthenticationResult(false, "账号或密码不正确 (admin / 123456)", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AuthenticationResult(true, null, "Sharp 管理员");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
namespace Client.Services;
|
|
||||||
|
|
||||||
public interface IUserSession
|
|
||||||
{
|
|
||||||
bool IsAuthenticated { get; }
|
|
||||||
string? UserName { get; }
|
|
||||||
string? DisplayName { get; }
|
|
||||||
void SignIn(string userName, string? displayName);
|
|
||||||
void SignOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class UserSession : IUserSession
|
|
||||||
{
|
|
||||||
public bool IsAuthenticated { get; private set; }
|
|
||||||
public string? UserName { get; private set; }
|
|
||||||
public string? DisplayName { get; private set; }
|
|
||||||
|
|
||||||
public void SignIn(string userName, string? displayName)
|
|
||||||
{
|
|
||||||
IsAuthenticated = true;
|
|
||||||
UserName = userName;
|
|
||||||
DisplayName = string.IsNullOrWhiteSpace(displayName) ? userName : displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SignOut()
|
|
||||||
{
|
|
||||||
IsAuthenticated = false;
|
|
||||||
UserName = null;
|
|
||||||
DisplayName = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Client.Services;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
|
|
||||||
namespace Client.ViewModels;
|
|
||||||
|
|
||||||
public sealed class LoginViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IAuthenticationService _authenticationService;
|
|
||||||
private readonly IUserSession _userSession;
|
|
||||||
private readonly IAsyncCommand _loginCommand;
|
|
||||||
|
|
||||||
private string _userName = "admin";
|
|
||||||
private string _password = string.Empty;
|
|
||||||
private string? _errorMessage;
|
|
||||||
|
|
||||||
public event EventHandler? LoginSucceeded;
|
|
||||||
|
|
||||||
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession)
|
|
||||||
{
|
|
||||||
_authenticationService = authenticationService;
|
|
||||||
_userSession = userSession;
|
|
||||||
_loginCommand = new AsyncCommand(LoginAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string UserName
|
|
||||||
{
|
|
||||||
get => _userName;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _userName, value))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Password
|
|
||||||
{
|
|
||||||
get => _password;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _password, value))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? ErrorMessage
|
|
||||||
{
|
|
||||||
get => _errorMessage;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _errorMessage, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(HasError));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
|
||||||
|
|
||||||
public IAsyncCommand LoginCommand => _loginCommand;
|
|
||||||
|
|
||||||
private async Task LoginAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
ErrorMessage = null;
|
|
||||||
|
|
||||||
var result = await _authenticationService.LoginAsync(UserName, Password);
|
|
||||||
|
|
||||||
if (!result.Succeeded)
|
|
||||||
{
|
|
||||||
ErrorMessage = result.Error;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_userSession.SignIn(UserName, result.DisplayName);
|
|
||||||
LoginSucceeded?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Client.Services;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using Sharp.Wpf.Services;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Common;
|
|
||||||
using Client.Hardware;
|
|
||||||
using Plc.Proxy;
|
|
||||||
using Plc.Abstractions;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using Sharp.Wpf.Abstractions;
|
|
||||||
using Sharp.Wpf.DevTools.Views;
|
|
||||||
|
|
||||||
namespace Client.ViewModels;
|
|
||||||
|
|
||||||
public sealed class MainViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IDialogService _dialogService;
|
|
||||||
private readonly IUserSession _userSession;
|
|
||||||
private readonly IDispatcher _dispatcher;
|
|
||||||
|
|
||||||
private string _greeting = "欢迎使用 Sharp";
|
|
||||||
private string _statusMessage = "系统就绪";
|
|
||||||
private DateTime _lastSyncTime = DateTime.Now;
|
|
||||||
|
|
||||||
// 魔法设备对象:直接绑定到界面,无需手动写 Read/Write/Subscribe
|
|
||||||
public IPackMachine Machine { get; }
|
|
||||||
|
|
||||||
public MainViewModel(IDialogService dialogService, IUserSession userSession, IDispatcher dispatcher)
|
|
||||||
{
|
|
||||||
_dialogService = dialogService;
|
|
||||||
_userSession = userSession;
|
|
||||||
_dispatcher = dispatcher;
|
|
||||||
_greeting = $"欢迎回来,{UserDisplayName}";
|
|
||||||
_statusMessage = "准备就绪";
|
|
||||||
|
|
||||||
ShowDialogCommand = new AsyncCommand(ShowDialogAsync);
|
|
||||||
RefreshDashboardCommand = new AsyncCommand(RefreshDashboardAsync);
|
|
||||||
ThrowCommand = new RelayCommand(() => throw new InvalidOperationException("演示:未处理异常将被全局处理"));
|
|
||||||
|
|
||||||
OpenPlcToolCommand = new RelayCommand(() =>
|
|
||||||
{
|
|
||||||
var sp = Global.Get<IServiceProvider>();
|
|
||||||
var win = sp.GetRequiredService<PlcWindow>();
|
|
||||||
win.Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
OpenSerialPortToolCommand = new RelayCommand(() =>
|
|
||||||
{
|
|
||||||
var win = new SerialPortWindow();
|
|
||||||
win.Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
OpenProcessTestCommand = new RelayCommand(() =>
|
|
||||||
{
|
|
||||||
var sp = Global.Get<IServiceProvider>();
|
|
||||||
var win = sp.GetRequiredService<ProcessTestWindow>();
|
|
||||||
win.Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
// DevTools 窗口命令
|
|
||||||
OpenDevToolsCommand = new RelayCommand(() =>
|
|
||||||
{
|
|
||||||
var sp = Global.Get<IServiceProvider>();
|
|
||||||
var win = sp.GetRequiredService<DevToolsWindow>();
|
|
||||||
win.Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
OpenLogViewerCommand = new RelayCommand(() =>
|
|
||||||
{
|
|
||||||
var sp = Global.Get<IServiceProvider>();
|
|
||||||
var win = sp.GetRequiredService<LogViewerWindow>();
|
|
||||||
win.Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
OpenPerformanceMonitorCommand = new RelayCommand(() =>
|
|
||||||
{
|
|
||||||
var sp = Global.Get<IServiceProvider>();
|
|
||||||
var win = sp.GetRequiredService<PerformanceMonitorWindow>();
|
|
||||||
win.Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
OpenAboutCommand = new RelayCommand(() =>
|
|
||||||
{
|
|
||||||
var sp = Global.Get<IServiceProvider>();
|
|
||||||
var win = sp.GetRequiredService<AboutWindow>();
|
|
||||||
win.ShowDialog();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1. 获取 PLC 服务
|
|
||||||
var plcService = Global.Get<IServiceProvider>().GetRequiredService<IPlcService>();
|
|
||||||
|
|
||||||
// 2. 创建代理
|
|
||||||
Machine = PlcInterfaceProxy.Create<IPackMachine>(plcService);
|
|
||||||
|
|
||||||
// 3. 代码监听演示:监听属性变化
|
|
||||||
Machine.PropertyChanged += OnMachinePropertyChanged;
|
|
||||||
|
|
||||||
// 4. 控制命令
|
|
||||||
StartMachineCommand = new RelayCommand(() => Machine.IsStartButtonPushed = true);
|
|
||||||
StopMachineCommand = new RelayCommand(() => Machine.IsStartButtonPushed = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMachinePropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
// 演示:当速度变化时,在日志或状态栏显示
|
|
||||||
if (e.PropertyName == nameof(IPackMachine.RunningSpeed))
|
|
||||||
{
|
|
||||||
_dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
if (Machine.RunningSpeed > 1000)
|
|
||||||
{
|
|
||||||
StatusMessage = $"警告:速度过高 ({Machine.RunningSpeed})";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//...
|
|
||||||
|
|
||||||
public string UserDisplayName => _userSession.DisplayName ?? _userSession.UserName ?? "访客";
|
|
||||||
|
|
||||||
public string Greeting
|
|
||||||
{
|
|
||||||
get => _greeting;
|
|
||||||
set => SetProperty(ref _greeting, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusMessage
|
|
||||||
{
|
|
||||||
get => _statusMessage;
|
|
||||||
set => SetProperty(ref _statusMessage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTime LastSyncTime
|
|
||||||
{
|
|
||||||
get => _lastSyncTime;
|
|
||||||
set => SetProperty(ref _lastSyncTime, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncCommand ShowDialogCommand { get; }
|
|
||||||
public IAsyncCommand RefreshDashboardCommand { get; }
|
|
||||||
public ICommand ThrowCommand { get; }
|
|
||||||
public ICommand OpenPlcToolCommand { get; }
|
|
||||||
public ICommand OpenSerialPortToolCommand { get; }
|
|
||||||
public ICommand OpenProcessTestCommand { get; }
|
|
||||||
public ICommand StartMachineCommand { get; }
|
|
||||||
public ICommand StopMachineCommand { get; }
|
|
||||||
|
|
||||||
// DevTools 命令
|
|
||||||
public ICommand OpenDevToolsCommand { get; }
|
|
||||||
public ICommand OpenLogViewerCommand { get; }
|
|
||||||
public ICommand OpenPerformanceMonitorCommand { get; }
|
|
||||||
public ICommand OpenAboutCommand { get; }
|
|
||||||
|
|
||||||
private async Task ShowDialogAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
await Task.Delay(300).ConfigureAwait(false);
|
|
||||||
await _dialogService.ShowInfoAsync($"Hi {UserDisplayName}, 这是来自 Sharp.Wpf 的提醒!").ConfigureAwait(false);
|
|
||||||
Greeting = $"问候更新时间: {DateTime.Now:HH:mm:ss}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshDashboardAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
StatusMessage = "正在同步仪表盘数据...";
|
|
||||||
await Task.Delay(600).ConfigureAwait(false);
|
|
||||||
LastSyncTime = DateTime.Now;
|
|
||||||
StatusMessage = "同步完成";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using Plc.Models;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Client.ViewModels;
|
|
||||||
|
|
||||||
public class PlcTagViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly PlcTag _tag;
|
|
||||||
|
|
||||||
public PlcTagViewModel(PlcTag tag)
|
|
||||||
{
|
|
||||||
_tag = tag ?? throw new ArgumentNullException(nameof(tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id => _tag.Id;
|
|
||||||
|
|
||||||
public string Address => _tag.Address;
|
|
||||||
|
|
||||||
public Type DataType => _tag.DataType;
|
|
||||||
|
|
||||||
public string DataTypeName => _tag.DataType == typeof(float) ? "Float" : _tag.DataType.Name;
|
|
||||||
|
|
||||||
public object? Value => _tag.Value;
|
|
||||||
|
|
||||||
public PlcTag Tag => _tag;
|
|
||||||
|
|
||||||
public void UpdateValue()
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Plc.Abstractions;
|
|
||||||
using Plc.Models;
|
|
||||||
using Plc.Siemens.Options;
|
|
||||||
using S7.Net;
|
|
||||||
using System.Windows;
|
|
||||||
using Sharp.Wpf.Abstractions;
|
|
||||||
|
|
||||||
namespace Client.ViewModels;
|
|
||||||
|
|
||||||
public class PlcViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IPlcService _plcService;
|
|
||||||
private readonly SiemensPlcOptions _siemensOptions;
|
|
||||||
private readonly IDispatcher _dispatcher;
|
|
||||||
|
|
||||||
// 连接配置
|
|
||||||
private PlcBrand _selectedBrand = PlcBrand.Siemens;
|
|
||||||
private CpuType _selectedCpuType = CpuType.S71200;
|
|
||||||
private string _ipAddress = "127.0.0.1";
|
|
||||||
private string _status = "未连接";
|
|
||||||
|
|
||||||
// 读写测试
|
|
||||||
private string _readAddress = "DB1.DBW0";
|
|
||||||
private string _readValue = "-";
|
|
||||||
private string _writeAddress = "DB1.DBW0";
|
|
||||||
private string _writeValue = "0";
|
|
||||||
|
|
||||||
// 监控
|
|
||||||
private string _monitorAddress = "DB1.DBW0";
|
|
||||||
private Type _selectedMonitorType = typeof(short);
|
|
||||||
|
|
||||||
public PlcViewModel(
|
|
||||||
IPlcService plcService,
|
|
||||||
SiemensPlcOptions siemensOptions,
|
|
||||||
IDispatcher dispatcher)
|
|
||||||
{
|
|
||||||
_plcService = plcService;
|
|
||||||
_siemensOptions = siemensOptions;
|
|
||||||
_dispatcher = dispatcher;
|
|
||||||
|
|
||||||
// 初始化界面值为当前配置值
|
|
||||||
_ipAddress = _siemensOptions.IpAddress;
|
|
||||||
_selectedCpuType = _siemensOptions.CpuType;
|
|
||||||
|
|
||||||
ConnectCommand = new AsyncCommand(ConnectAsync);
|
|
||||||
DisconnectCommand = new AsyncCommand(DisconnectAsync);
|
|
||||||
ReadCommand = new AsyncCommand(ReadAsync);
|
|
||||||
WriteCommand = new AsyncCommand(WriteAsync);
|
|
||||||
AddMonitorCommand = new RelayCommand(AddMonitor);
|
|
||||||
RemoveMonitorCommand = new RelayCommand<PlcTagViewModel>(RemoveMonitor, tag => tag != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
|
|
||||||
public PlcBrand SelectedBrand
|
|
||||||
{
|
|
||||||
get => _selectedBrand;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _selectedBrand, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(IsSiemensOptionsVisible));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CpuType SelectedCpuType
|
|
||||||
{
|
|
||||||
get => _selectedCpuType;
|
|
||||||
set => SetProperty(ref _selectedCpuType, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSiemensOptionsVisible => SelectedBrand == PlcBrand.Siemens;
|
|
||||||
|
|
||||||
public string IpAddress
|
|
||||||
{
|
|
||||||
get => _ipAddress;
|
|
||||||
set => SetProperty(ref _ipAddress, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Status
|
|
||||||
{
|
|
||||||
get => _status;
|
|
||||||
set => SetProperty(ref _status, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ReadAddress
|
|
||||||
{
|
|
||||||
get => _readAddress;
|
|
||||||
set => SetProperty(ref _readAddress, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ReadValue
|
|
||||||
{
|
|
||||||
get => _readValue;
|
|
||||||
set => SetProperty(ref _readValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string WriteAddress
|
|
||||||
{
|
|
||||||
get => _writeAddress;
|
|
||||||
set => SetProperty(ref _writeAddress, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string WriteValue
|
|
||||||
{
|
|
||||||
get => _writeValue;
|
|
||||||
set => SetProperty(ref _writeValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string MonitorAddress
|
|
||||||
{
|
|
||||||
get => _monitorAddress;
|
|
||||||
set => SetProperty(ref _monitorAddress, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type SelectedMonitorType
|
|
||||||
{
|
|
||||||
get => _selectedMonitorType;
|
|
||||||
set => SetProperty(ref _selectedMonitorType, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据源
|
|
||||||
public ObservableCollection<PlcBrand> Brands { get; } = new(Enum.GetValues<PlcBrand>());
|
|
||||||
public ObservableCollection<CpuType> CpuTypes { get; } = new(Enum.GetValues<CpuType>());
|
|
||||||
public ObservableCollection<DataTypeOption> SupportedMonitorTypes { get; } = new()
|
|
||||||
{
|
|
||||||
new("Bool", typeof(bool)),
|
|
||||||
new("Byte", typeof(byte)),
|
|
||||||
new("Short (Int16)", typeof(short)),
|
|
||||||
new("UShort (UInt16)", typeof(ushort)),
|
|
||||||
new("Int (Int32)", typeof(int)),
|
|
||||||
new("UInt (UInt32)", typeof(uint)),
|
|
||||||
new("Long (Int64)", typeof(long)),
|
|
||||||
new("ULong (UInt64)", typeof(ulong)),
|
|
||||||
new("Float (Real)", typeof(float)),
|
|
||||||
new("Double (LReal)", typeof(double)),
|
|
||||||
new("String", typeof(string))
|
|
||||||
};
|
|
||||||
public ObservableCollection<PlcTagViewModel> MonitoredTags { get; } = new();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Commands
|
|
||||||
|
|
||||||
public IAsyncCommand ConnectCommand { get; }
|
|
||||||
public IAsyncCommand DisconnectCommand { get; }
|
|
||||||
public IAsyncCommand ReadCommand { get; }
|
|
||||||
public IAsyncCommand WriteCommand { get; }
|
|
||||||
public ICommand AddMonitorCommand { get; }
|
|
||||||
public ICommand RemoveMonitorCommand { get; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private async Task ConnectAsync()
|
|
||||||
{
|
|
||||||
if (SelectedBrand != PlcBrand.Siemens)
|
|
||||||
{
|
|
||||||
Status = $"暂不支持 {SelectedBrand},请等待后续更新";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status = "正在连接...";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 1. 更新配置 (因为 SiemensDriver 注册为单例,且引用了 SiemensPlcOptions 单例)
|
|
||||||
// 修改这里的 Options 会直接影响 Driver 下一次 ConnectAsync 的行为
|
|
||||||
_siemensOptions.IpAddress = IpAddress;
|
|
||||||
_siemensOptions.CpuType = SelectedCpuType;
|
|
||||||
// 端口、机架号等暂时保持默认,也可以暴露在界面上
|
|
||||||
|
|
||||||
// 2. 启动服务 (内部会调用 Driver.ConnectAsync)
|
|
||||||
await _plcService.StartAsync();
|
|
||||||
Status = $"已连接 ({SelectedBrand} - {SelectedCpuType})";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Status = $"连接失败: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DisconnectAsync()
|
|
||||||
{
|
|
||||||
await _plcService.StopAsync();
|
|
||||||
Status = "已断开";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReadAsync()
|
|
||||||
{
|
|
||||||
var result = await _plcService.ReadAsync<short>(ReadAddress);
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
ReadValue = result.Value.ToString();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ReadValue = $"Error: {result.ErrorMessage}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task WriteAsync()
|
|
||||||
{
|
|
||||||
if (short.TryParse(WriteValue, out var val))
|
|
||||||
{
|
|
||||||
var result = await _plcService.WriteAsync(WriteAddress, val);
|
|
||||||
if (result.IsFailure)
|
|
||||||
{
|
|
||||||
Status = $"写入失败: {result.ErrorMessage}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Status = "写入成功";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Status = "输入值无效 (仅支持 short)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddMonitor()
|
|
||||||
{
|
|
||||||
var tag = new PlcTag
|
|
||||||
{
|
|
||||||
Address = MonitorAddress,
|
|
||||||
DataType = SelectedMonitorType
|
|
||||||
};
|
|
||||||
|
|
||||||
var vm = new PlcTagViewModel(tag);
|
|
||||||
|
|
||||||
tag.OnChanged = (t, v) =>
|
|
||||||
{
|
|
||||||
// 使用Dispatcher在UI线程更新值,避免跨线程访问异常
|
|
||||||
_dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
vm.UpdateValue();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_plcService.Subscribe(tag);
|
|
||||||
|
|
||||||
// 在UI线程添加到集合
|
|
||||||
_dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
MonitoredTags.Add(vm);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveMonitor(PlcTagViewModel? tagVm)
|
|
||||||
{
|
|
||||||
if (tagVm == null) return;
|
|
||||||
|
|
||||||
// 从PLC服务取消订阅
|
|
||||||
_plcService.Unsubscribe(tagVm.Id);
|
|
||||||
|
|
||||||
// 从UI集合移除
|
|
||||||
_dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
MonitoredTags.Remove(tagVm);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DataTypeOption(string Name, Type Type);
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Sharp.Wpf.Abstractions;
|
|
||||||
using Sharp.Process;
|
|
||||||
using Sharp.Python;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Client.ViewModels;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process/Python 测试工具 ViewModel
|
|
||||||
/// </summary>
|
|
||||||
public class ProcessTestViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IProcessExecutor _processExecutor;
|
|
||||||
private readonly IPythonExecutor _pythonExecutor;
|
|
||||||
private readonly IDispatcher _dispatcher;
|
|
||||||
private readonly ILogger<ProcessTestViewModel> _logger;
|
|
||||||
|
|
||||||
// Process 测试
|
|
||||||
private string _command = "cmd";
|
|
||||||
private string _arguments = "/c echo Hello from Process!";
|
|
||||||
private int _timeoutSeconds = 30;
|
|
||||||
private string _processOutput = "输出将显示在这里...";
|
|
||||||
private string _processStatus = "就绪";
|
|
||||||
|
|
||||||
// Python 测试
|
|
||||||
private string _pythonPath = "(自动检测)";
|
|
||||||
private string _pythonVersion = "未检测";
|
|
||||||
private string _pythonCode = "print('Hello from Python!')";
|
|
||||||
private string _pythonScriptPath = "";
|
|
||||||
private string _pythonArgs = "";
|
|
||||||
private string _pythonOutput = "输出将显示在这里...";
|
|
||||||
private string _pythonStatus = "就绪";
|
|
||||||
|
|
||||||
// 日志
|
|
||||||
public ObservableCollection<string> LogMessages { get; } = new();
|
|
||||||
|
|
||||||
public ProcessTestViewModel(
|
|
||||||
IProcessExecutor processExecutor,
|
|
||||||
IPythonExecutor pythonExecutor,
|
|
||||||
IDispatcher dispatcher,
|
|
||||||
ILogger<ProcessTestViewModel> logger)
|
|
||||||
{
|
|
||||||
_processExecutor = processExecutor;
|
|
||||||
_pythonExecutor = pythonExecutor;
|
|
||||||
_dispatcher = dispatcher;
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
// Process 命令
|
|
||||||
RunProcessCommand = new AsyncCommand(RunProcessAsync);
|
|
||||||
RunShellCommand = new AsyncCommand(RunShellAsync);
|
|
||||||
TestDotnetCommand = new AsyncCommand(TestDotnetAsync);
|
|
||||||
TestGitCommand = new AsyncCommand(TestGitAsync);
|
|
||||||
|
|
||||||
// Python 命令
|
|
||||||
DetectPythonCommand = new AsyncCommand(DetectPythonAsync);
|
|
||||||
RunPythonCodeCommand = new AsyncCommand(RunPythonCodeAsync);
|
|
||||||
RunPythonScriptCommand = new AsyncCommand(RunPythonScriptAsync);
|
|
||||||
TestPythonJsonCommand = new AsyncCommand(TestPythonJsonAsync);
|
|
||||||
CreateTestScriptCommand = new RelayCommand(CreateTestScript);
|
|
||||||
|
|
||||||
// 其他
|
|
||||||
ClearLogCommand = new RelayCommand(() => LogMessages.Clear());
|
|
||||||
|
|
||||||
// 自动检测 Python
|
|
||||||
_ = DetectPythonAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
|
|
||||||
public string Command
|
|
||||||
{
|
|
||||||
get => _command;
|
|
||||||
set => SetProperty(ref _command, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Arguments
|
|
||||||
{
|
|
||||||
get => _arguments;
|
|
||||||
set => SetProperty(ref _arguments, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int TimeoutSeconds
|
|
||||||
{
|
|
||||||
get => _timeoutSeconds;
|
|
||||||
set => SetProperty(ref _timeoutSeconds, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ProcessOutput
|
|
||||||
{
|
|
||||||
get => _processOutput;
|
|
||||||
set => SetProperty(ref _processOutput, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ProcessStatus
|
|
||||||
{
|
|
||||||
get => _processStatus;
|
|
||||||
set => SetProperty(ref _processStatus, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PythonPath
|
|
||||||
{
|
|
||||||
get => _pythonPath;
|
|
||||||
set => SetProperty(ref _pythonPath, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PythonVersion
|
|
||||||
{
|
|
||||||
get => _pythonVersion;
|
|
||||||
set => SetProperty(ref _pythonVersion, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PythonCode
|
|
||||||
{
|
|
||||||
get => _pythonCode;
|
|
||||||
set => SetProperty(ref _pythonCode, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PythonScriptPath
|
|
||||||
{
|
|
||||||
get => _pythonScriptPath;
|
|
||||||
set => SetProperty(ref _pythonScriptPath, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PythonArgs
|
|
||||||
{
|
|
||||||
get => _pythonArgs;
|
|
||||||
set => SetProperty(ref _pythonArgs, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PythonOutput
|
|
||||||
{
|
|
||||||
get => _pythonOutput;
|
|
||||||
set => SetProperty(ref _pythonOutput, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PythonStatus
|
|
||||||
{
|
|
||||||
get => _pythonStatus;
|
|
||||||
set => SetProperty(ref _pythonStatus, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Commands
|
|
||||||
|
|
||||||
public IAsyncCommand RunProcessCommand { get; }
|
|
||||||
public IAsyncCommand RunShellCommand { get; }
|
|
||||||
public IAsyncCommand TestDotnetCommand { get; }
|
|
||||||
public IAsyncCommand TestGitCommand { get; }
|
|
||||||
|
|
||||||
public IAsyncCommand DetectPythonCommand { get; }
|
|
||||||
public IAsyncCommand RunPythonCodeCommand { get; }
|
|
||||||
public IAsyncCommand RunPythonScriptCommand { get; }
|
|
||||||
public IAsyncCommand TestPythonJsonCommand { get; }
|
|
||||||
public ICommand CreateTestScriptCommand { get; }
|
|
||||||
|
|
||||||
public ICommand ClearLogCommand { get; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Process 测试
|
|
||||||
|
|
||||||
private async Task RunProcessAsync()
|
|
||||||
{
|
|
||||||
ProcessStatus = "执行中...";
|
|
||||||
ProcessOutput = "";
|
|
||||||
AddLog($"[Process] 执行命令: {Command} {Arguments}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var options = new ProcessOptions
|
|
||||||
{
|
|
||||||
Command = Command,
|
|
||||||
Arguments = Arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
|
||||||
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
|
|
||||||
OnOutputReceived = line =>
|
|
||||||
{
|
|
||||||
_dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
ProcessOutput += line + "\n";
|
|
||||||
OnPropertyChanged(nameof(ProcessOutput));
|
|
||||||
AddLog($"[输出] {line}");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
OnErrorReceived = line =>
|
|
||||||
{
|
|
||||||
_dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
ProcessOutput += $"[错误] {line}\n";
|
|
||||||
OnPropertyChanged(nameof(ProcessOutput));
|
|
||||||
AddLog($"[错误] {line}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = await _processExecutor.ExecuteAsync(options);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
ProcessStatus = $"✓ 完成 (退出码: {result.Value.ExitCode}, 耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
|
||||||
ProcessOutput += $"\n=== 执行成功 ===\n退出码: {result.Value.ExitCode}\n耗时: {result.Value.Duration.TotalSeconds:F2}s";
|
|
||||||
AddLog($"[Process] 执行成功 (耗时: {result.Value.Duration.TotalMilliseconds}ms)");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ProcessStatus = $"✗ 失败: {result.ErrorMessage}";
|
|
||||||
ProcessOutput += $"\n=== 执行失败 ===\n{result.ErrorMessage}";
|
|
||||||
AddLog($"[Process] 执行失败: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ProcessStatus = $"✗ 异常: {ex.Message}";
|
|
||||||
ProcessOutput = $"异常: {ex.Message}\n{ex.StackTrace}";
|
|
||||||
AddLog($"[Process] 异常: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunShellAsync()
|
|
||||||
{
|
|
||||||
ProcessStatus = "执行中...";
|
|
||||||
ProcessOutput = "";
|
|
||||||
|
|
||||||
var shellCmd = OperatingSystem.IsWindows()
|
|
||||||
? "dir | findstr .cs"
|
|
||||||
: "ls | grep .cs";
|
|
||||||
|
|
||||||
AddLog($"[Shell] 执行: {shellCmd}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await IProcessExecutor.RunShellAsync(shellCmd);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
ProcessOutput = result.Value.StandardOutput;
|
|
||||||
ProcessStatus = $"✓ Shell 命令执行成功";
|
|
||||||
AddLog($"[Shell] 成功");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ProcessOutput = result.ErrorMessage ?? "未知错误";
|
|
||||||
ProcessStatus = $"✗ Shell 命令失败";
|
|
||||||
AddLog($"[Shell] 失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ProcessStatus = $"✗ 异常: {ex.Message}";
|
|
||||||
ProcessOutput = ex.Message;
|
|
||||||
AddLog($"[Shell] 异常: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TestDotnetAsync()
|
|
||||||
{
|
|
||||||
Command = "dotnet";
|
|
||||||
Arguments = "--version";
|
|
||||||
await RunProcessAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TestGitAsync()
|
|
||||||
{
|
|
||||||
Command = "git";
|
|
||||||
Arguments = "--version";
|
|
||||||
await RunProcessAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Python 测试
|
|
||||||
|
|
||||||
private async Task DetectPythonAsync()
|
|
||||||
{
|
|
||||||
PythonStatus = "检测中...";
|
|
||||||
AddLog("[Python] 正在检测 Python 环境...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var env = PythonEnvironment.Discover();
|
|
||||||
if (env != null)
|
|
||||||
{
|
|
||||||
PythonPath = env.PythonPath;
|
|
||||||
PythonVersion = env.Version ?? "未知版本";
|
|
||||||
PythonStatus = "✓ Python 环境已检测";
|
|
||||||
AddLog($"[Python] 找到 Python: {env.PythonPath}");
|
|
||||||
AddLog($"[Python] 版本: {env.Version}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PythonPath = "未找到";
|
|
||||||
PythonVersion = "未安装";
|
|
||||||
PythonStatus = "✗ 未找到 Python";
|
|
||||||
AddLog("[Python] 未找到 Python 环境,请安装 Python 或配置 PATH");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PythonStatus = $"✗ 检测失败: {ex.Message}";
|
|
||||||
AddLog($"[Python] 检测异常: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunPythonCodeAsync()
|
|
||||||
{
|
|
||||||
PythonStatus = "执行中...";
|
|
||||||
PythonOutput = "";
|
|
||||||
AddLog($"[Python] 执行代码: {PythonCode}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _pythonExecutor.RunCodeAsync(PythonCode, new PythonOptions
|
|
||||||
{
|
|
||||||
OnOutputReceived = line =>
|
|
||||||
{
|
|
||||||
_dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
PythonOutput += line + "\n";
|
|
||||||
OnPropertyChanged(nameof(PythonOutput));
|
|
||||||
AddLog($"[Python 输出] {line}");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
OnErrorReceived = line =>
|
|
||||||
{
|
|
||||||
_dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
PythonOutput += $"[错误] {line}\n";
|
|
||||||
OnPropertyChanged(nameof(PythonOutput));
|
|
||||||
AddLog($"[Python 错误] {line}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(PythonOutput))
|
|
||||||
{
|
|
||||||
PythonOutput = result.Value.StandardOutput;
|
|
||||||
}
|
|
||||||
PythonStatus = $"✓ 执行成功 (耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
|
||||||
AddLog($"[Python] 执行成功");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PythonOutput = result.ErrorMessage ?? "未知错误";
|
|
||||||
PythonStatus = $"✗ 执行失败";
|
|
||||||
AddLog($"[Python] 执行失败: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PythonStatus = $"✗ 异常: {ex.Message}";
|
|
||||||
PythonOutput = ex.Message;
|
|
||||||
AddLog($"[Python] 异常: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunPythonScriptAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(PythonScriptPath) || !File.Exists(PythonScriptPath))
|
|
||||||
{
|
|
||||||
PythonStatus = "✗ 请选择有效的脚本文件";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PythonStatus = "执行中...";
|
|
||||||
PythonOutput = "";
|
|
||||||
AddLog($"[Python] 执行脚本: {PythonScriptPath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 解析参数(简单的 key=value 格式)
|
|
||||||
object? args = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(PythonArgs))
|
|
||||||
{
|
|
||||||
var argsDict = new Dictionary<string, string>();
|
|
||||||
var pairs = PythonArgs.Split(',', StringSplitOptions.TrimEntries);
|
|
||||||
foreach (var pair in pairs)
|
|
||||||
{
|
|
||||||
var kv = pair.Split('=', 2);
|
|
||||||
if (kv.Length == 2)
|
|
||||||
{
|
|
||||||
argsDict[kv[0].Trim()] = kv[1].Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
args = argsDict;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _pythonExecutor.RunScriptAsync(
|
|
||||||
PythonScriptPath,
|
|
||||||
args,
|
|
||||||
new PythonOptions
|
|
||||||
{
|
|
||||||
OnOutputReceived = line =>
|
|
||||||
{
|
|
||||||
_dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
PythonOutput += line + "\n";
|
|
||||||
OnPropertyChanged(nameof(PythonOutput));
|
|
||||||
AddLog($"[Script] {line}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(PythonOutput))
|
|
||||||
{
|
|
||||||
PythonOutput = result.Value.StandardOutput;
|
|
||||||
}
|
|
||||||
PythonStatus = $"✓ 脚本执行成功";
|
|
||||||
AddLog($"[Python] 脚本执行成功");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PythonOutput = result.ErrorMessage ?? "未知错误";
|
|
||||||
PythonStatus = $"✗ 脚本执行失败";
|
|
||||||
AddLog($"[Python] 脚本执行失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PythonStatus = $"✗ 异常: {ex.Message}";
|
|
||||||
PythonOutput = ex.Message;
|
|
||||||
AddLog($"[Python] 异常: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TestPythonJsonAsync()
|
|
||||||
{
|
|
||||||
PythonCode = @"import json
|
|
||||||
result = {'status': 'ok', 'value': 42, 'message': '测试成功'}
|
|
||||||
print(json.dumps(result, ensure_ascii=False))";
|
|
||||||
await RunPythonCodeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateTestScript()
|
|
||||||
{
|
|
||||||
var tempPath = Path.Combine(Path.GetTempPath(), "sharp_test.py");
|
|
||||||
|
|
||||||
var scriptContent = @"import sys
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('--name', default='World')
|
|
||||||
parser.add_argument('--count', type=int, default=1)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# 日志输出到 stderr
|
|
||||||
print(f'参数: name={args.name}, count={args.count}', file=sys.stderr)
|
|
||||||
|
|
||||||
# 处理
|
|
||||||
for i in range(args.count):
|
|
||||||
print(f'Hello, {args.name}! ({i+1})', file=sys.stderr)
|
|
||||||
|
|
||||||
# 结果输出到 stdout(JSON)
|
|
||||||
result = {
|
|
||||||
'message': f'Greeted {args.name} {args.count} times',
|
|
||||||
'success': True,
|
|
||||||
'timestamp': str(__import__('datetime').datetime.now())
|
|
||||||
}
|
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.WriteAllText(tempPath, scriptContent);
|
|
||||||
PythonScriptPath = tempPath;
|
|
||||||
PythonArgs = "name=Sharp,count=3";
|
|
||||||
PythonStatus = $"✓ 测试脚本已创建: {tempPath}";
|
|
||||||
AddLog($"[Python] 测试脚本已创建: {tempPath}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PythonStatus = $"✗ 创建脚本失败: {ex.Message}";
|
|
||||||
AddLog($"[Python] 创建脚本失败: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private void AddLog(string message)
|
|
||||||
{
|
|
||||||
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
|
||||||
_dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
LogMessages.Add($"[{timestamp}] {message}");
|
|
||||||
|
|
||||||
// 保持日志数量在合理范围
|
|
||||||
while (LogMessages.Count > 200)
|
|
||||||
{
|
|
||||||
LogMessages.RemoveAt(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"System": {
|
|
||||||
"HTTPIp": "127.0.0.1",
|
|
||||||
"HttpPort": 8080,
|
|
||||||
"TimeoutSeconds": 30,
|
|
||||||
"Scheme": "http"
|
|
||||||
},
|
|
||||||
"NetServer": {
|
|
||||||
"UseLogging": true,
|
|
||||||
"UseExceptionHandling": true,
|
|
||||||
"UseCors": true,
|
|
||||||
"UseRateLimit": true,
|
|
||||||
"UseResponseCache": true
|
|
||||||
},
|
|
||||||
"NetClient": {
|
|
||||||
"Timeout": "00:00:20",
|
|
||||||
"EnableLogging": true,
|
|
||||||
"IncludeCorrelationId": true,
|
|
||||||
"CorrelationIdHeaderName": "X-Correlation-Id",
|
|
||||||
"UseBearerToken": false,
|
|
||||||
"Retry": {
|
|
||||||
"Enabled": true,
|
|
||||||
"MaxRetryAttempts": 3,
|
|
||||||
"FirstDelay": "00:00:01",
|
|
||||||
"Handle429": true,
|
|
||||||
"HandleRequestTimeout": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SharpLog": {
|
|
||||||
"UseSerilog": true,
|
|
||||||
"UseConsole": true,
|
|
||||||
"ConsoleJson": false,
|
|
||||||
"MinLevel": "Information",
|
|
||||||
"Overrides": {
|
|
||||||
"Microsoft": "Warning"
|
|
||||||
},
|
|
||||||
"File": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Path": "logs/app-.log",
|
|
||||||
"Rolling": "Day",
|
|
||||||
"RetainedFileCountLimit": 7,
|
|
||||||
"Json": true
|
|
||||||
},
|
|
||||||
"Enrich": {
|
|
||||||
"Enabled": true,
|
|
||||||
"WithThreadId": true,
|
|
||||||
"WithProcessId": true,
|
|
||||||
"WithMachineName": true,
|
|
||||||
"WithEnvironmentName": true,
|
|
||||||
"WithActivityTrace": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "0048F04EA9C102506BBFAC3803E4711F700CF3ED"
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Automation;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Controls.Primitives;
|
|
||||||
using System.Windows.Controls.Ribbon;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Forms.Integration;
|
|
||||||
using System.Windows.Ink;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Markup;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Animation;
|
|
||||||
using System.Windows.Media.Effects;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Media.Media3D;
|
|
||||||
using System.Windows.Media.TextFormatting;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
using System.Windows.Shell;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Client {
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// App
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : System.Windows.Application {
|
|
||||||
|
|
||||||
private bool _contentLoaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// InitializeComponent
|
|
||||||
/// </summary>
|
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.0.0")]
|
|
||||||
public void InitializeComponent() {
|
|
||||||
if (_contentLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_contentLoaded = true;
|
|
||||||
System.Uri resourceLocater = new System.Uri("/Client;component/app.xaml", System.UriKind.Relative);
|
|
||||||
|
|
||||||
#line 1 "..\..\..\App.xaml"
|
|
||||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
|
||||||
|
|
||||||
#line default
|
|
||||||
#line hidden
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Application Entry Point.
|
|
||||||
/// </summary>
|
|
||||||
[System.STAThreadAttribute()]
|
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.0.0")]
|
|
||||||
public static void Main() {
|
|
||||||
Client.App app = new Client.App();
|
|
||||||
app.InitializeComponent();
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+433e206d456d0ef3b2fc57c458ff3fc030643be6")]
|
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
|
||||||
[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")]
|
|
||||||
[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")]
|
|
||||||
|
|
||||||
// 由 MSBuild WriteCodeFragment 类生成。
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
de45fc2ddda59d691d6bfd3f2673675ae92a6f2f53cc261c9aeefe56c546545d
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
is_global = true
|
|
||||||
build_property.TargetFramework = net8.0-windows
|
|
||||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
|
||||||
build_property.TargetFrameworkVersion = v8.0
|
|
||||||
build_property.TargetPlatformMinVersion = 7.0
|
|
||||||
build_property.UsingMicrosoftNETSdkWeb =
|
|
||||||
build_property.ProjectTypeGuids =
|
|
||||||
build_property.InvariantGlobalization =
|
|
||||||
build_property.PlatformNeutralAssembly =
|
|
||||||
build_property.EnforceExtendedAnalyzerRules =
|
|
||||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
|
||||||
build_property.RootNamespace = Client
|
|
||||||
build_property.ProjectDir = D:\code\sharp-basic\src\Sharp\Client\
|
|
||||||
build_property.EnableComHosting =
|
|
||||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
|
||||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
|
||||||
build_property.EffectiveAnalysisLevelStyle = 8.0
|
|
||||||
build_property.EnableCodeStyleSeverity =
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<Application x:Class="Server.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:local="clr-namespace:Server">
|
|
||||||
<!-- HandyControl 主题资源已通过 AddSharpHandyControl() 自动注册,无需手动添加 -->
|
|
||||||
<Application.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Windows;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Config;
|
|
||||||
using Sharp.Log.Extensions;
|
|
||||||
using Sharp.Data;
|
|
||||||
using Sharp.Data.Context;
|
|
||||||
using Sharp.Wpf.Services;
|
|
||||||
using Sharp.Wpf.Abstractions;
|
|
||||||
using Sharp.Wpf.HandyControl.DependencyInjection;
|
|
||||||
using Server.Config;
|
|
||||||
using Server.Services;
|
|
||||||
using Server.Data;
|
|
||||||
using Server.ViewModels;
|
|
||||||
using Server.Views;
|
|
||||||
|
|
||||||
namespace Server
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : Application
|
|
||||||
{
|
|
||||||
private ServiceProvider? _serviceProvider;
|
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnStartup(e);
|
|
||||||
|
|
||||||
// 1. Global Exception Handling
|
|
||||||
DispatcherUnhandledException += (s, args) =>
|
|
||||||
{
|
|
||||||
// Safe logging: handle case where DI failed
|
|
||||||
if (_serviceProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
|
||||||
logger?.LogError(args.Exception, "UI线程未处理异常");
|
|
||||||
}
|
|
||||||
catch { /* Ignore logging failure during crash */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageBox.Show($"发生未处理异常: {args.Exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
args.Handled = true; // Prevent crash
|
|
||||||
};
|
|
||||||
|
|
||||||
TaskScheduler.UnobservedTaskException += (s, args) =>
|
|
||||||
{
|
|
||||||
if (_serviceProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
|
||||||
logger?.LogError(args.Exception, "任务未观察到的异常");
|
|
||||||
}
|
|
||||||
catch { /* Ignore */ }
|
|
||||||
}
|
|
||||||
args.SetObserved();
|
|
||||||
};
|
|
||||||
|
|
||||||
AppDomain.CurrentDomain.UnhandledException += (s, args) =>
|
|
||||||
{
|
|
||||||
var ex = args.ExceptionObject as Exception;
|
|
||||||
if (_serviceProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
|
||||||
logger?.LogCritical(ex, "AppDomain未处理异常 (即将终止: {IsTerminating})", args.IsTerminating);
|
|
||||||
}
|
|
||||||
catch { /* Ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.IsTerminating)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"发生严重错误,程序即将退出: {ex?.Message}", "严重错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 2. 构建配置
|
|
||||||
var cm = ConfigDefaults.BuildDefault(defaults =>
|
|
||||||
{
|
|
||||||
defaults.MainFile = "appsettings.json";
|
|
||||||
defaults.IncludeEnvironmentVariables = true;
|
|
||||||
}, configure =>
|
|
||||||
{
|
|
||||||
configure.ReloadOnChange = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. 构建依赖注入容器
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
|
|
||||||
// 配置管理
|
|
||||||
services.AddSingleton(cm);
|
|
||||||
services.AddSingleton<IConfiguration>(cm.Configuration);
|
|
||||||
var appConfig = cm.Get<AppConfig>(validate: true);
|
|
||||||
services.AddSingleton(appConfig);
|
|
||||||
services.AddSingleton(appConfig.Server);
|
|
||||||
|
|
||||||
// 日志
|
|
||||||
services.AddLogging(b => b.AddSharpLog());
|
|
||||||
|
|
||||||
// 数据库
|
|
||||||
services.AddSingleton<IDataContext>(sp =>
|
|
||||||
{
|
|
||||||
var builder = new DataContextBuilder<AppDbContext>();
|
|
||||||
builder.UseSqlite("Data Source=server.db")
|
|
||||||
.UseAudit(true)
|
|
||||||
.UseSoftDelete(true)
|
|
||||||
.UseCurrentUser("system")
|
|
||||||
.EnableDetailedErrors(true);
|
|
||||||
return builder.Build();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 服务器服务
|
|
||||||
services.AddSingleton<ServerService>();
|
|
||||||
|
|
||||||
// WPF服务
|
|
||||||
services.AddSharpHandyControl(); // HandyControl 对话框服务(也注册了 IDialogService)
|
|
||||||
services.AddSingleton<IDispatcher>(new DispatcherService(Dispatcher));
|
|
||||||
|
|
||||||
// ViewModels
|
|
||||||
services.AddTransient<MainViewModel>();
|
|
||||||
services.AddTransient<UserManagementViewModel>();
|
|
||||||
|
|
||||||
// Windows
|
|
||||||
services.AddTransient<MainWindow>();
|
|
||||||
services.AddTransient<UserManagementWindow>();
|
|
||||||
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
|
||||||
|
|
||||||
// 4. 显示主窗口
|
|
||||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
|
||||||
Current.MainWindow = mainWindow;
|
|
||||||
mainWindow.Show();
|
|
||||||
|
|
||||||
var logger = _serviceProvider.GetRequiredService<ILogger<App>>();
|
|
||||||
logger.LogInformation("应用程序启动成功");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show(
|
|
||||||
$"应用程序初始化失败: {ex.Message}\n\n{ex.StackTrace}",
|
|
||||||
"启动错误",
|
|
||||||
MessageBoxButton.OK,
|
|
||||||
MessageBoxImage.Error);
|
|
||||||
|
|
||||||
Shutdown(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_serviceProvider?.GetService<ServerService>()?.Dispose();
|
|
||||||
_serviceProvider?.GetService<IDataContext>()?.Dispose();
|
|
||||||
|
|
||||||
var logger = _serviceProvider?.GetService<ILogger<App>>();
|
|
||||||
logger?.LogInformation("应用程序正常退出");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// 忽略退出时的异常
|
|
||||||
Console.WriteLine($"退出时出错: {ex.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_serviceProvider?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
base.OnExit(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
[assembly: ThemeInfo(
|
|
||||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// or application resource dictionaries)
|
|
||||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// app, or any theme specific resource dictionaries)
|
|
||||||
)]
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Server.Config
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 服务器配置
|
|
||||||
/// </summary>
|
|
||||||
public class ServerConfig
|
|
||||||
{
|
|
||||||
[Range(1, 65535)]
|
|
||||||
public int Port { get; set; } = 8080;
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string Host { get; set; } = "localhost";
|
|
||||||
|
|
||||||
[Range(1, 10000)]
|
|
||||||
public int MaxConcurrentRequests { get; set; } = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 应用配置(根配置)
|
|
||||||
/// </summary>
|
|
||||||
public class AppConfig
|
|
||||||
{
|
|
||||||
public ServerConfig Server { get; set; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
using Sharp.NetServer.Attributes;
|
|
||||||
using Sharp.NetServer.Controllers;
|
|
||||||
using Sharp.NetServer.Extensions;
|
|
||||||
using Sharp.NetServer.Models;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace Server.Controllers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 数据操作控制器(演示CRUD操作)
|
|
||||||
/// </summary>
|
|
||||||
[Route("/api/data")]
|
|
||||||
public class DataController : BaseController
|
|
||||||
{
|
|
||||||
// 内存存储(演示用)
|
|
||||||
private static readonly ConcurrentDictionary<string, DataItem> _dataStore = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取所有数据
|
|
||||||
/// GET /api/data
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet]
|
|
||||||
public async Task GetAll(HttpContext context)
|
|
||||||
{
|
|
||||||
var items = _dataStore.Values.ToList();
|
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
|
||||||
{
|
|
||||||
Count = items.Count,
|
|
||||||
Items = items
|
|
||||||
});
|
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取指定ID的数据
|
|
||||||
/// GET /api/data/123
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public async Task GetById(HttpContext context)
|
|
||||||
{
|
|
||||||
var id = context.Request.Url.Segments.LastOrDefault()?.TrimEnd('/');
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
await BadRequestAsync(context, "Invalid ID");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_dataStore.TryGetValue(id, out var item))
|
|
||||||
{
|
|
||||||
var result = ResultExtensions.HttpSuccess(item);
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var result = ResultExtensions.HttpFailure<object>($"Item with id '{id}' not found", 404);
|
|
||||||
await NotFoundAsync(context, result.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建新数据
|
|
||||||
/// POST /api/data
|
|
||||||
/// Body: { "name": "test", "value": "data" }
|
|
||||||
/// </summary>
|
|
||||||
[HttpPost]
|
|
||||||
public async Task Create(HttpContext context)
|
|
||||||
{
|
|
||||||
var request = await ReadJsonBodyAsync<CreateDataRequest>(context);
|
|
||||||
|
|
||||||
if (request == null || string.IsNullOrWhiteSpace(request.Name))
|
|
||||||
{
|
|
||||||
await BadRequestAsync(context, "Name is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = Guid.NewGuid().ToString();
|
|
||||||
var item = new DataItem
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Name = request.Name,
|
|
||||||
Value = request.Value,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
UpdatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
_dataStore[id] = item;
|
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(item, "Data created successfully");
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 更新数据
|
|
||||||
/// PUT /api/data/123
|
|
||||||
/// Body: { "name": "updated", "value": "new data" }
|
|
||||||
/// </summary>
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
public async Task Update(HttpContext context)
|
|
||||||
{
|
|
||||||
var id = context.Request.Url.Segments.LastOrDefault()?.TrimEnd('/');
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
await BadRequestAsync(context, "Invalid ID");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_dataStore.TryGetValue(id, out var item))
|
|
||||||
{
|
|
||||||
await NotFoundAsync(context, $"Item with id '{id}' not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = await ReadJsonBodyAsync<UpdateDataRequest>(context);
|
|
||||||
if (request != null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Name))
|
|
||||||
{
|
|
||||||
item.Name = request.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Value != null)
|
|
||||||
{
|
|
||||||
item.Value = request.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.UpdatedAt = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(item, "Data updated successfully");
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除数据
|
|
||||||
/// DELETE /api/data/123
|
|
||||||
/// </summary>
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task Delete(HttpContext context)
|
|
||||||
{
|
|
||||||
var id = context.Request.Url.Segments.LastOrDefault()?.TrimEnd('/');
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
await BadRequestAsync(context, "Invalid ID");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_dataStore.TryRemove(id, out var item))
|
|
||||||
{
|
|
||||||
var result = ResultExtensions.HttpSuccess(item, "Data deleted successfully");
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await NotFoundAsync(context, $"Item with id '{id}' not found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 清空所有数据
|
|
||||||
/// DELETE /api/data
|
|
||||||
/// </summary>
|
|
||||||
[HttpDelete]
|
|
||||||
public async Task Clear(HttpContext context)
|
|
||||||
{
|
|
||||||
var count = _dataStore.Count;
|
|
||||||
_dataStore.Clear();
|
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(new { DeletedCount = count }, "All data cleared");
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Request/Response Models
|
|
||||||
|
|
||||||
public class CreateDataRequest
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UpdateDataRequest
|
|
||||||
{
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DataItem
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? Value { get; set; }
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public DateTime UpdatedAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Sharp.NetServer.Attributes;
|
|
||||||
using Sharp.NetServer.Controllers;
|
|
||||||
using Sharp.NetServer.Extensions;
|
|
||||||
using Sharp.NetServer.Models;
|
|
||||||
|
|
||||||
namespace Server.Controllers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 健康检查控制器
|
|
||||||
/// </summary>
|
|
||||||
[Route("/api/health")]
|
|
||||||
public class HealthController : BaseController
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 健康检查端点
|
|
||||||
/// GET /api/health
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet]
|
|
||||||
public async Task Check(HttpContext context)
|
|
||||||
{
|
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
|
||||||
{
|
|
||||||
Status = "Healthy",
|
|
||||||
Timestamp = DateTime.UtcNow,
|
|
||||||
Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime
|
|
||||||
});
|
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 详细健康检查
|
|
||||||
/// GET /api/health/detailed
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("/detailed")]
|
|
||||||
public async Task DetailedCheck(HttpContext context)
|
|
||||||
{
|
|
||||||
var process = Process.GetCurrentProcess();
|
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
|
||||||
{
|
|
||||||
Status = "Healthy",
|
|
||||||
Timestamp = DateTime.UtcNow,
|
|
||||||
System = new
|
|
||||||
{
|
|
||||||
MachineName = Environment.MachineName,
|
|
||||||
OSVersion = Environment.OSVersion.ToString(),
|
|
||||||
ProcessorCount = Environment.ProcessorCount,
|
|
||||||
Is64BitOS = Environment.Is64BitOperatingSystem
|
|
||||||
},
|
|
||||||
Process = new
|
|
||||||
{
|
|
||||||
Id = process.Id,
|
|
||||||
Name = process.ProcessName,
|
|
||||||
StartTime = process.StartTime,
|
|
||||||
Memory = new
|
|
||||||
{
|
|
||||||
WorkingSet = $"{process.WorkingSet64 / 1024 / 1024} MB",
|
|
||||||
PrivateMemory = $"{process.PrivateMemorySize64 / 1024 / 1024} MB"
|
|
||||||
},
|
|
||||||
Threads = process.Threads.Count
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
using Sharp.NetServer.Attributes;
|
|
||||||
using Sharp.NetServer.Controllers;
|
|
||||||
using Sharp.NetServer.Extensions;
|
|
||||||
using Sharp.NetServer.Models;
|
|
||||||
|
|
||||||
namespace Server.Controllers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 系统信息控制器
|
|
||||||
/// </summary>
|
|
||||||
[Route("/api/system")]
|
|
||||||
public class SystemController : BaseController
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取系统信息
|
|
||||||
/// GET /api/system/info
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("/info")]
|
|
||||||
public async Task GetSystemInfo(HttpContext context)
|
|
||||||
{
|
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
|
||||||
{
|
|
||||||
MachineName = Environment.MachineName,
|
|
||||||
UserName = Environment.UserName,
|
|
||||||
OSVersion = Environment.OSVersion.ToString(),
|
|
||||||
ProcessorCount = Environment.ProcessorCount,
|
|
||||||
Is64BitOS = Environment.Is64BitOperatingSystem,
|
|
||||||
Is64BitProcess = Environment.Is64BitProcess,
|
|
||||||
SystemDirectory = Environment.SystemDirectory,
|
|
||||||
CurrentDirectory = Environment.CurrentDirectory,
|
|
||||||
TickCount = Environment.TickCount64,
|
|
||||||
Version = Environment.Version.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取环境变量
|
|
||||||
/// GET /api/system/env?name=PATH
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("/env")]
|
|
||||||
public async Task GetEnvironmentVariable(HttpContext context)
|
|
||||||
{
|
|
||||||
var name = GetQueryParam(context, "name", "");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
var result = ResultExtensions.HttpFailure<object>("Environment variable name is required", 400);
|
|
||||||
await WriteErrorResponseAsync(context, result.Message, 400);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = Environment.GetEnvironmentVariable(name);
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
var result = ResultExtensions.HttpFailure<object>($"Environment variable '{name}' not found", 404);
|
|
||||||
await NotFoundAsync(context, result.Message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var successResult = ResultExtensions.HttpSuccess(new { Name = name, Value = value });
|
|
||||||
await WriteSuccessResponseAsync(context, successResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前时间
|
|
||||||
/// GET /api/system/time
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("/time")]
|
|
||||||
public async Task GetCurrentTime(HttpContext context)
|
|
||||||
{
|
|
||||||
var now = DateTime.Now;
|
|
||||||
var utcNow = DateTime.UtcNow;
|
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
|
||||||
{
|
|
||||||
Local = new
|
|
||||||
{
|
|
||||||
DateTime = now,
|
|
||||||
Date = now.Date,
|
|
||||||
Time = now.TimeOfDay,
|
|
||||||
DayOfWeek = now.DayOfWeek.ToString()
|
|
||||||
},
|
|
||||||
UTC = new
|
|
||||||
{
|
|
||||||
DateTime = utcNow,
|
|
||||||
Date = utcNow.Date,
|
|
||||||
Time = utcNow.TimeOfDay,
|
|
||||||
UnixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Sharp.Data.Entities;
|
|
||||||
|
|
||||||
namespace Server.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 示例用户实体 - 使用审计基类
|
|
||||||
/// </summary>
|
|
||||||
public class User : AuditableEntity
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 用户名
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 邮箱
|
|
||||||
/// </summary>
|
|
||||||
public string Email { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 年龄
|
|
||||||
/// </summary>
|
|
||||||
public int Age { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否激活
|
|
||||||
/// </summary>
|
|
||||||
public bool IsActive { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 备注
|
|
||||||
/// </summary>
|
|
||||||
public string? Remarks { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 应用数据库上下文
|
|
||||||
/// </summary>
|
|
||||||
public class AppDbContext : DbContext
|
|
||||||
{
|
|
||||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public DbSet<User> Users => Set<User>();
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
// 配置 User 实体
|
|
||||||
modelBuilder.Entity<User>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
entity.Property(e => e.UserName).IsRequired().HasMaxLength(50);
|
|
||||||
entity.Property(e => e.Email).IsRequired().HasMaxLength(100);
|
|
||||||
entity.Property(e => e.Remarks).HasMaxLength(500);
|
|
||||||
|
|
||||||
// 添加索引
|
|
||||||
entity.HasIndex(e => e.UserName);
|
|
||||||
entity.HasIndex(e => e.Email).IsUnique();
|
|
||||||
entity.HasIndex(e => e.IsActive);
|
|
||||||
entity.HasIndex(e => e.CreatedAt);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加示例数据
|
|
||||||
modelBuilder.Entity<User>().HasData(
|
|
||||||
new User
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
UserName = "admin",
|
|
||||||
Email = "admin@example.com",
|
|
||||||
Age = 30,
|
|
||||||
IsActive = true,
|
|
||||||
Remarks = "系统管理员",
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
CreatedBy = "system"
|
|
||||||
},
|
|
||||||
new User
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
UserName = "zhangsan",
|
|
||||||
Email = "zhangsan@example.com",
|
|
||||||
Age = 25,
|
|
||||||
IsActive = true,
|
|
||||||
Remarks = "测试用户1",
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
CreatedBy = "system"
|
|
||||||
},
|
|
||||||
new User
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
UserName = "lisi",
|
|
||||||
Email = "lisi@example.com",
|
|
||||||
Age = 28,
|
|
||||||
IsActive = true,
|
|
||||||
Remarks = "测试用户2",
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
CreatedBy = "system"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Sharp.NetServer;
|
|
||||||
|
|
||||||
namespace Server.Extensions;
|
|
||||||
|
|
||||||
internal static class HttpServerBuilderLoggingExtensions
|
|
||||||
{
|
|
||||||
public static HttpServerBuilder UseLoggerFactory(this HttpServerBuilder builder, ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
builder.ConfigureLogging(services =>
|
|
||||||
{
|
|
||||||
services.AddSingleton(loggerFactory);
|
|
||||||
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
|
|
||||||
});
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Server.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:Server"
|
|
||||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="Sharp HTTP Server" Height="600" Width="900"
|
|
||||||
WindowStartupLocation="CenterScreen">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 标题栏 -->
|
|
||||||
<Border Grid.Row="0" Background="#2196F3" Padding="15" CornerRadius="5" Margin="0,0,0,10">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="Sharp HTTP Server" FontSize="24" FontWeight="Bold" Foreground="White"/>
|
|
||||||
<TextBlock Text="基于 NetServer 框架的 HTTP 服务器演示" FontSize="12" Foreground="White" Opacity="0.9"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 服务器控制面板 -->
|
|
||||||
<Border Grid.Row="1" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="15" Margin="0,0,0,10">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- 服务器信息 -->
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="服务器地址:" VerticalAlignment="Center" Margin="0,0,10,5"/>
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ServerUrl}"
|
|
||||||
FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="服务器状态:" VerticalAlignment="Center" Margin="0,0,10,5"/>
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding StatusText}"
|
|
||||||
Foreground="{Binding StatusColor}" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/>
|
|
||||||
|
|
||||||
<!-- 控制按钮 -->
|
|
||||||
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<Button Content="➕ 启动服务器" Width="120" Height="32" Margin="0,0,10,0"
|
|
||||||
Command="{Binding StartCommand}"
|
|
||||||
IsEnabled="{Binding CanStart}"
|
|
||||||
Style="{StaticResource ButtonSuccess}"/>
|
|
||||||
<Button Content="⏹ 停止服务器" Width="120" Height="32" Margin="0,0,10,0"
|
|
||||||
Command="{Binding StopCommand}"
|
|
||||||
IsEnabled="{Binding IsRunning}"
|
|
||||||
Style="{StaticResource ButtonDanger}"/>
|
|
||||||
<Button Content="🔄 重启服务器" Width="120" Height="32" Margin="0,0,10,0"
|
|
||||||
Command="{Binding RestartCommand}"
|
|
||||||
IsEnabled="{Binding IsRunning}"
|
|
||||||
Style="{StaticResource ButtonWarning}"/>
|
|
||||||
<Button Content="📊 数据管理" Width="120" Height="32" Margin="0,0,10,0"
|
|
||||||
Command="{Binding OpenDataManagementCommand}"
|
|
||||||
Style="{StaticResource ButtonPrimary}"/>
|
|
||||||
<Button Content="🗑 清空日志" Width="100" Height="32"
|
|
||||||
Command="{Binding ClearLogCommand}"
|
|
||||||
Style="{StaticResource ButtonDefault}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 日志显示区域 -->
|
|
||||||
<Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="10">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Text="日志信息" FontSize="14" FontWeight="Bold" Margin="0,0,0,10"/>
|
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
|
||||||
<TextBox Text="{Binding LogText, Mode=OneWay}"
|
|
||||||
IsReadOnly="True"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Background="#F5F5F5"
|
|
||||||
BorderThickness="0"
|
|
||||||
FontFamily="Consolas"
|
|
||||||
FontSize="11"
|
|
||||||
Padding="5"/>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- API 端点提示 -->
|
|
||||||
<Border Grid.Row="3" Background="#E3F2FD" Padding="10" CornerRadius="5" Margin="0,10,0,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="可用的 API 端点:" FontWeight="Bold" Margin="0,0,0,5"/>
|
|
||||||
<TextBlock FontSize="11">
|
|
||||||
<Run Text="• GET /api/health - 健康检查"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• GET /api/health/detailed - 详细健康检查"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• GET /api/system/info - 系统信息"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• GET /api/system/time - 当前时间"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• GET /api/data - 获取所有数据"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• POST /api/data - 创建数据 (Body: {"name":"test","value":"data"})"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• GET /api/data/{id} - 获取指定数据"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• PUT /api/data/{id} - 更新数据"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• DELETE /api/data/{id} - 删除数据"/>
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using HandyControl.Controls;
|
|
||||||
using Server.ViewModels;
|
|
||||||
|
|
||||||
namespace Server
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MainWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : GlowWindow
|
|
||||||
{
|
|
||||||
public MainWindow(MainViewModel viewModel)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
DataContext = viewModel;
|
|
||||||
|
|
||||||
// Handle window closing event
|
|
||||||
Closing += (s, e) =>
|
|
||||||
{
|
|
||||||
// Check if we need to stop the server before closing
|
|
||||||
e.Cancel = !viewModel.CheckBeforeClose();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Sharp.Server
|
|
||||||
|
|
||||||
基于 WPF 的服务器控制台/仪表盘应用,集成了 `Sharp.NetServer` 作为内置 HTTP 服务端,演示了“桌面应用 + Web 服务”的混合架构。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- **可视化控制**:通过 WPF 界面启动/停止 HTTP 服务。
|
|
||||||
- **实时日志**:在界面上显示服务器运行日志(集成 `Sharp.Log`)。
|
|
||||||
- **数据管理**:内置 SQLite 数据库管理界面(集成 `Sharp.Data`)。
|
|
||||||
- **混合架构**:
|
|
||||||
- 前台:WPF UI 用于运维和监控。
|
|
||||||
- 后台:`NetServer` 提供 REST API 供局域网设备/客户端调用。
|
|
||||||
|
|
||||||
## 架构说明
|
|
||||||
|
|
||||||
### 核心组件
|
|
||||||
- **ServerService**: 封装 `HttpServer` 的生命周期管理(启动、停止、重启)。
|
|
||||||
- **AppConfig**: 使用 `Sharp.Config` 映射的强类型配置。
|
|
||||||
- **Controllers**: API 控制器,处理来自网络的 HTTP 请求。
|
|
||||||
|
|
||||||
### 启动流程 (App.xaml.cs)
|
|
||||||
1. **配置**:加载 `appsettings.json`。
|
|
||||||
2. **日志**:初始化 Serilog 并绑定到 UI 输出。
|
|
||||||
3. **数据库**:初始化 EF Core Context。
|
|
||||||
4. **Web服务**:实例化 `ServerService` 但不立即启动(由用户点击按钮触发,或配置为自动启动)。
|
|
||||||
|
|
||||||
## 配置 (appsettings.json)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Server": {
|
|
||||||
"Host": "0.0.0.0",
|
|
||||||
"Port": 5000,
|
|
||||||
"UseAuth": true
|
|
||||||
},
|
|
||||||
"Database": { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
1. 启动应用。
|
|
||||||
2. 点击“启动服务”按钮。
|
|
||||||
3. 状态栏显示“运行中”,并显示监听地址(如 `http://localhost:5000`)。
|
|
||||||
4. 使用浏览器或 Postman 访问 API(如 `/api/health`)。
|
|
||||||
5. 点击“数据管理”可查看存储在本地 SQLite 中的数据。
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<SelfContained>true</SelfContained>
|
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
|
||||||
<PublishTrimmed>false</PublishTrimmed>
|
|
||||||
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\NetServer\NetServer.csproj" />
|
|
||||||
<ProjectReference Include="..\Log\Log.csproj" />
|
|
||||||
<ProjectReference Include="..\Config\Config.csproj" />
|
|
||||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
|
||||||
<ProjectReference Include="..\Wpf.HandyControl\Wpf.HandyControl.csproj" />
|
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
|
||||||
<ProjectReference Include="..\Data\Data.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="appsettings.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup />
|
|
||||||
</Project>
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Sharp.NetServer;
|
|
||||||
using Sharp.NetServer.Server;
|
|
||||||
using Server.Config;
|
|
||||||
using Server.Controllers;
|
|
||||||
using Server.Extensions;
|
|
||||||
|
|
||||||
namespace Server.Services
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// HTTP服务器服务,管理服务器生命周期
|
|
||||||
/// </summary>
|
|
||||||
public class ServerService
|
|
||||||
{
|
|
||||||
private HttpServer? _httpServer;
|
|
||||||
private Task? _serverTask;
|
|
||||||
private CancellationTokenSource? _cts;
|
|
||||||
private readonly AppConfig _config;
|
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
|
||||||
private readonly ILogger<ServerService> _logger;
|
|
||||||
|
|
||||||
public bool IsRunning { get; private set; }
|
|
||||||
|
|
||||||
public event EventHandler<string>? StatusChanged;
|
|
||||||
|
|
||||||
public ServerService(AppConfig config, ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_loggerFactory = loggerFactory;
|
|
||||||
_logger = loggerFactory.CreateLogger<ServerService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 启动服务器
|
|
||||||
/// </summary>
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (IsRunning)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Server is already running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Starting HTTP server on {Host}:{Port}", _config.Server.Host, _config.Server.Port);
|
|
||||||
OnStatusChanged($"正在启动服务器 {_config.Server.Host}:{_config.Server.Port}...");
|
|
||||||
|
|
||||||
var builder = new HttpServerBuilder()
|
|
||||||
.UseUrls($"http://{_config.Server.Host}:{_config.Server.Port}/")
|
|
||||||
.UseLoggerFactory(_loggerFactory)
|
|
||||||
.ConfigureServices(services =>
|
|
||||||
{
|
|
||||||
// 注册配置
|
|
||||||
services.AddSingleton(_config);
|
|
||||||
services.AddSingleton(_config.Server);
|
|
||||||
})
|
|
||||||
// 注册控制器(从当前程序集)
|
|
||||||
.AddControllers<ServerService>();
|
|
||||||
|
|
||||||
_httpServer = builder.Build();
|
|
||||||
|
|
||||||
// 在后台启动服务器
|
|
||||||
_cts = new CancellationTokenSource();
|
|
||||||
_serverTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _httpServer.StartAsync(_cts.Token);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Server task error");
|
|
||||||
OnStatusChanged($"服务器运行错误: {ex.Message}");
|
|
||||||
IsRunning = false;
|
|
||||||
}
|
|
||||||
}, _cts.Token);
|
|
||||||
|
|
||||||
// 等待服务器启动
|
|
||||||
Thread.Sleep(500);
|
|
||||||
|
|
||||||
IsRunning = true;
|
|
||||||
_logger.LogInformation("HTTP server started successfully on {Host}:{Port}", _config.Server.Host, _config.Server.Port);
|
|
||||||
OnStatusChanged($"服务器已启动: http://{_config.Server.Host}:{_config.Server.Port}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
IsRunning = false;
|
|
||||||
_logger.LogError(ex, "Failed to start HTTP server");
|
|
||||||
OnStatusChanged($"启动失败: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 停止服务器
|
|
||||||
/// </summary>
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!IsRunning)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Server is not running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Stopping HTTP server");
|
|
||||||
OnStatusChanged("正在停止服务器...");
|
|
||||||
|
|
||||||
_cts?.Cancel();
|
|
||||||
_httpServer?.Stop();
|
|
||||||
_serverTask?.Wait(TimeSpan.FromSeconds(5));
|
|
||||||
|
|
||||||
IsRunning = false;
|
|
||||||
_logger.LogInformation("HTTP server stopped successfully");
|
|
||||||
OnStatusChanged("服务器已停止");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to stop HTTP server");
|
|
||||||
OnStatusChanged($"停止失败: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 重启服务器
|
|
||||||
/// </summary>
|
|
||||||
public void Restart()
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Restarting HTTP server");
|
|
||||||
OnStatusChanged("正在重启服务器...");
|
|
||||||
|
|
||||||
Stop();
|
|
||||||
Thread.Sleep(500); // 等待端口释放
|
|
||||||
Start();
|
|
||||||
|
|
||||||
OnStatusChanged("服务器已重启");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnStatusChanged(string status)
|
|
||||||
{
|
|
||||||
StatusChanged?.Invoke(this, status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (IsRunning)
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
_cts?.Dispose();
|
|
||||||
_serverTask?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Server.Config;
|
|
||||||
using Server.Services;
|
|
||||||
using Server.Views;
|
|
||||||
|
|
||||||
namespace Server.ViewModels
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 主窗口ViewModel
|
|
||||||
/// </summary>
|
|
||||||
public class MainViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly ServerService _serverService;
|
|
||||||
private readonly ILogger<MainViewModel> _logger;
|
|
||||||
private readonly IServiceProvider _serviceProvider;
|
|
||||||
|
|
||||||
private string _statusText = "未启动";
|
|
||||||
private Brush _statusColor = Brushes.Gray;
|
|
||||||
private string _logText = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 构造函数
|
|
||||||
/// </summary>
|
|
||||||
public MainViewModel(
|
|
||||||
ServerService serverService,
|
|
||||||
AppConfig config,
|
|
||||||
ILogger<MainViewModel> logger,
|
|
||||||
IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
_serverService = serverService ?? throw new ArgumentNullException(nameof(serverService));
|
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
||||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
|
||||||
|
|
||||||
ServerUrl = $"http://{config.Server.Host}:{config.Server.Port}";
|
|
||||||
|
|
||||||
// 初始化命令
|
|
||||||
StartCommand = new RelayCommand(_ => Start(), _ => CanStart);
|
|
||||||
StopCommand = new RelayCommand(_ => Stop(), _ => IsRunning);
|
|
||||||
RestartCommand = new RelayCommand(_ => Restart(), _ => IsRunning);
|
|
||||||
ClearLogCommand = new RelayCommand(_ => ClearLog());
|
|
||||||
OpenDataManagementCommand = new RelayCommand(_ => OpenDataManagement());
|
|
||||||
|
|
||||||
// 订阅服务器状态变化事件
|
|
||||||
_serverService.StatusChanged += OnServerStatusChanged;
|
|
||||||
|
|
||||||
AppendLog("应用程序已启动");
|
|
||||||
AppendLog($"配置已加载: {config.Server.Host}:{config.Server.Port}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 服务器URL
|
|
||||||
/// </summary>
|
|
||||||
public string ServerUrl { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 状态文本
|
|
||||||
/// </summary>
|
|
||||||
public string StatusText
|
|
||||||
{
|
|
||||||
get => _statusText;
|
|
||||||
set => SetProperty(ref _statusText, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 状态颜色
|
|
||||||
/// </summary>
|
|
||||||
public Brush StatusColor
|
|
||||||
{
|
|
||||||
get => _statusColor;
|
|
||||||
set => SetProperty(ref _statusColor, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 日志文本
|
|
||||||
/// </summary>
|
|
||||||
public string LogText
|
|
||||||
{
|
|
||||||
get => _logText;
|
|
||||||
set => SetProperty(ref _logText, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 服务器是否正在运行
|
|
||||||
/// </summary>
|
|
||||||
public bool IsRunning => _serverService.IsRunning;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 能否启动服务器
|
|
||||||
/// </summary>
|
|
||||||
public bool CanStart => !IsRunning;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 启动命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand StartCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 停止命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand StopCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 重启命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand RestartCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 清除日志命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand ClearLogCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 打开数据管理命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand OpenDataManagementCommand { get; }
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_serverService.Start();
|
|
||||||
UpdateButtonStates();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "启动服务器失败");
|
|
||||||
AppendLog($"启动失败: {ex.Message}");
|
|
||||||
MessageBox.Show($"启动服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Stop()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_serverService.Stop();
|
|
||||||
UpdateButtonStates();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "停止服务器失败");
|
|
||||||
AppendLog($"停止失败: {ex.Message}");
|
|
||||||
MessageBox.Show($"停止服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Restart()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_serverService.Restart();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "重启服务器失败");
|
|
||||||
AppendLog($"重启失败: {ex.Message}");
|
|
||||||
MessageBox.Show($"重启服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearLog()
|
|
||||||
{
|
|
||||||
LogText = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenDataManagement()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("打开数据管理窗口");
|
|
||||||
var window = _serviceProvider.GetRequiredService<UserManagementWindow>();
|
|
||||||
window.Owner = Application.Current.MainWindow;
|
|
||||||
window.ShowDialog();
|
|
||||||
_logger.LogInformation("数据管理窗口已关闭");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "打开数据管理窗口失败");
|
|
||||||
AppendLog($"打开数据管理窗口失败: {ex.Message}");
|
|
||||||
MessageBox.Show($"打开数据管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnServerStatusChanged(object? sender, string status)
|
|
||||||
{
|
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
StatusText = status;
|
|
||||||
|
|
||||||
// 根据状态更新颜色
|
|
||||||
if (status.Contains("已启动"))
|
|
||||||
{
|
|
||||||
StatusColor = Brushes.Green;
|
|
||||||
}
|
|
||||||
else if (status.Contains("已停止"))
|
|
||||||
{
|
|
||||||
StatusColor = Brushes.Gray;
|
|
||||||
}
|
|
||||||
else if (status.Contains("失败") || status.Contains("错误"))
|
|
||||||
{
|
|
||||||
StatusColor = Brushes.Red;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
StatusColor = Brushes.Orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateButtonStates();
|
|
||||||
AppendLog(status);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateButtonStates()
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
|
||||||
OnPropertyChanged(nameof(CanStart));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendLog(string message)
|
|
||||||
{
|
|
||||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
||||||
LogText += $"[{timestamp}] {message}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查是否需要停止服务器
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>true表示可以关闭,false表示用户取消</returns>
|
|
||||||
public bool CheckBeforeClose()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_serverService.IsRunning)
|
|
||||||
{
|
|
||||||
var result = MessageBox.Show(
|
|
||||||
"服务器正在运行,是否停止服务器并退出?",
|
|
||||||
"确认退出",
|
|
||||||
MessageBoxButton.YesNo,
|
|
||||||
MessageBoxImage.Question);
|
|
||||||
|
|
||||||
if (result == MessageBoxResult.Yes)
|
|
||||||
{
|
|
||||||
_serverService.Stop();
|
|
||||||
AppendLog("服务器已停止");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "关闭窗口时出错");
|
|
||||||
AppendLog($"关闭出错: {ex.Message}");
|
|
||||||
return true; // 即使出错也允许关闭
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 释放资源
|
|
||||||
/// </summary>
|
|
||||||
public override void Dispose()
|
|
||||||
{
|
|
||||||
_serverService.StatusChanged -= OnServerStatusChanged;
|
|
||||||
base.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Sharp.Data.Context;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Sharp.Wpf.Abstractions;
|
|
||||||
using Sharp.Wpf.Services;
|
|
||||||
using Server.Data;
|
|
||||||
using Server.Views;
|
|
||||||
|
|
||||||
namespace Server.ViewModels
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 用户管理ViewModel
|
|
||||||
/// </summary>
|
|
||||||
public class UserManagementViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IDataContext _dataContext;
|
|
||||||
private readonly ILogger<UserManagementViewModel> _logger;
|
|
||||||
private readonly IServiceProvider _serviceProvider;
|
|
||||||
private readonly IDialogService _dialogService;
|
|
||||||
|
|
||||||
private User? _selectedUser;
|
|
||||||
private string _searchText = string.Empty;
|
|
||||||
private string _statusText = string.Empty;
|
|
||||||
private string _recordCount = "记录数: 0";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 构造函数
|
|
||||||
/// </summary>
|
|
||||||
public UserManagementViewModel(
|
|
||||||
IDataContext dataContext,
|
|
||||||
ILogger<UserManagementViewModel> logger,
|
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
IDialogService dialogService)
|
|
||||||
{
|
|
||||||
_dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext));
|
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
||||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
|
||||||
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
|
||||||
|
|
||||||
Users = new ObservableCollection<User>();
|
|
||||||
|
|
||||||
// 初始化命令
|
|
||||||
LoadDataCommand = new AsyncCommand(async () => await LoadDataAsync());
|
|
||||||
SearchCommand = new AsyncCommand(async () => await LoadDataAsync(SearchText.Trim()));
|
|
||||||
ClearSearchCommand = new AsyncCommand(async () =>
|
|
||||||
{
|
|
||||||
SearchText = string.Empty;
|
|
||||||
await LoadDataAsync();
|
|
||||||
});
|
|
||||||
AddCommand = new RelayCommand(_ => Add());
|
|
||||||
EditCommand = new RelayCommand(_ => Edit(), _ => SelectedUser != null);
|
|
||||||
DeleteCommand = new AsyncCommand(async () => await DeleteAsync(), () => SelectedUser != null);
|
|
||||||
RefreshCommand = new AsyncCommand(async () => await LoadDataAsync(SearchText.Trim()));
|
|
||||||
|
|
||||||
// 加载初始数据
|
|
||||||
_ = LoadDataAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户列表
|
|
||||||
/// </summary>
|
|
||||||
public ObservableCollection<User> Users { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 选中的用户
|
|
||||||
/// </summary>
|
|
||||||
public User? SelectedUser
|
|
||||||
{
|
|
||||||
get => _selectedUser;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _selectedUser, value))
|
|
||||||
{
|
|
||||||
((RelayCommand)EditCommand).RaiseCanExecuteChanged();
|
|
||||||
((AsyncCommand)DeleteCommand).RaiseCanExecuteChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 搜索文本
|
|
||||||
/// </summary>
|
|
||||||
public string SearchText
|
|
||||||
{
|
|
||||||
get => _searchText;
|
|
||||||
set => SetProperty(ref _searchText, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 状态文本
|
|
||||||
/// </summary>
|
|
||||||
public string StatusText
|
|
||||||
{
|
|
||||||
get => _statusText;
|
|
||||||
set => SetProperty(ref _statusText, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 记录数
|
|
||||||
/// </summary>
|
|
||||||
public string RecordCount
|
|
||||||
{
|
|
||||||
get => _recordCount;
|
|
||||||
set => SetProperty(ref _recordCount, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 加载数据命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand LoadDataCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 搜索命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand SearchCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 清除搜索命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand ClearSearchCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 添加命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand AddCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 编辑命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand EditCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand DeleteCommand { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 刷新命令
|
|
||||||
/// </summary>
|
|
||||||
public ICommand RefreshCommand { get; }
|
|
||||||
|
|
||||||
private async Task LoadDataAsync(string? searchText = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
StatusText = "正在加载数据...";
|
|
||||||
_logger.LogInformation("开始加载用户数据,搜索条件: {SearchText}", searchText ?? "无");
|
|
||||||
|
|
||||||
var repo = _dataContext.Repository<User>();
|
|
||||||
|
|
||||||
List<User> users;
|
|
||||||
if (string.IsNullOrWhiteSpace(searchText))
|
|
||||||
{
|
|
||||||
users = await repo.GetAllAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
users = await repo.FindAsync(u =>
|
|
||||||
u.UserName.Contains(searchText) ||
|
|
||||||
u.Email.Contains(searchText) ||
|
|
||||||
(u.Remarks != null && u.Remarks.Contains(searchText)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Users.Clear();
|
|
||||||
foreach (var user in users.OrderByDescending(u => u.CreatedAt))
|
|
||||||
{
|
|
||||||
Users.Add(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
RecordCount = $"记录数: {users.Count}";
|
|
||||||
StatusText = string.IsNullOrWhiteSpace(searchText)
|
|
||||||
? $"加载完成,共 {users.Count} 条记录"
|
|
||||||
: $"搜索完成,找到 {users.Count} 条记录";
|
|
||||||
|
|
||||||
_logger.LogInformation("加载完成,共 {Count} 条记录", users.Count);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "加载数据失败");
|
|
||||||
StatusText = "加载失败";
|
|
||||||
await _dialogService.ShowErrorAsync($"加载数据失败: {ex.Message}", "错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Add()
|
|
||||||
{
|
|
||||||
var dialog = new UserEditDialog();
|
|
||||||
if (dialog.ShowDialog() == true)
|
|
||||||
{
|
|
||||||
_ = SaveUserAsync(dialog.UserData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Edit()
|
|
||||||
{
|
|
||||||
if (SelectedUser == null)
|
|
||||||
{
|
|
||||||
_ = _dialogService.ShowInfoAsync("请选择要编辑的用户", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dialog = new UserEditDialog(SelectedUser);
|
|
||||||
if (dialog.ShowDialog() == true)
|
|
||||||
{
|
|
||||||
_ = UpdateUserAsync(dialog.UserData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteAsync()
|
|
||||||
{
|
|
||||||
if (SelectedUser == null)
|
|
||||||
{
|
|
||||||
await _dialogService.ShowInfoAsync("请选择要删除的用户", "提示");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var confirmed = await _dialogService.ShowConfirmAsync(
|
|
||||||
$"确定要删除用户 '{SelectedUser.UserName}' 吗?\n\n(注意:这是软删除,数据会被标记为已删除但不会真正删除)",
|
|
||||||
"确认删除");
|
|
||||||
|
|
||||||
if (confirmed)
|
|
||||||
{
|
|
||||||
await DeleteUserAsync(SelectedUser.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveUserAsync(User user)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
StatusText = "正在保存...";
|
|
||||||
_logger.LogInformation("新增用户: {UserName}", user.UserName);
|
|
||||||
|
|
||||||
var repo = _dataContext.Repository<User>();
|
|
||||||
await repo.AddAsync(user);
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("用户新增成功: {UserId}", user.Id);
|
|
||||||
await _dialogService.ShowInfoAsync("用户新增成功!", "成功");
|
|
||||||
|
|
||||||
await LoadDataAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "新增用户失败");
|
|
||||||
StatusText = "保存失败";
|
|
||||||
await _dialogService.ShowErrorAsync($"新增用户失败: {ex.Message}", "错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateUserAsync(User user)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
StatusText = "正在更新...";
|
|
||||||
_logger.LogInformation("更新用户: {UserId}", user.Id);
|
|
||||||
|
|
||||||
var repo = _dataContext.Repository<User>();
|
|
||||||
repo.Update(user);
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("用户更新成功: {UserId}", user.Id);
|
|
||||||
await _dialogService.ShowInfoAsync("用户更新成功!", "成功");
|
|
||||||
|
|
||||||
await LoadDataAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "更新用户失败");
|
|
||||||
StatusText = "更新失败";
|
|
||||||
await _dialogService.ShowErrorAsync($"更新用户失败: {ex.Message}", "错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteUserAsync(Guid userId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
StatusText = "正在删除...";
|
|
||||||
_logger.LogInformation("软删除用户: {UserId}", userId);
|
|
||||||
|
|
||||||
var repo = _dataContext.Repository<User>();
|
|
||||||
await repo.RemoveAsync(userId); // 软删除
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("用户软删除成功: {UserId}", userId);
|
|
||||||
await _dialogService.ShowInfoAsync("用户已标记为删除(软删除)!", "成功");
|
|
||||||
|
|
||||||
await LoadDataAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "删除用户失败");
|
|
||||||
StatusText = "删除失败";
|
|
||||||
await _dialogService.ShowErrorAsync($"删除用户失败: {ex.Message}", "错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<Window x:Class="Server.Views.UserEditDialog"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
Title="用户编辑" Height="450" Width="500"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
ResizeMode="NoResize">
|
|
||||||
<Grid Margin="20">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 标题 -->
|
|
||||||
<TextBlock Grid.Row="0" Text="用户信息" FontSize="18" FontWeight="Bold" Margin="0,0,0,20"/>
|
|
||||||
|
|
||||||
<!-- 表单 -->
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel>
|
|
||||||
<!-- 用户名 -->
|
|
||||||
<TextBlock Text="用户名 *" FontWeight="Bold" Margin="0,0,0,5"/>
|
|
||||||
<TextBox x:Name="UserNameTextBox" Height="32" Padding="8,0" Margin="0,0,0,15"
|
|
||||||
BorderBrush="#BDBDBD" BorderThickness="1"/>
|
|
||||||
|
|
||||||
<!-- 邮箱 -->
|
|
||||||
<TextBlock Text="邮箱 *" FontWeight="Bold" Margin="0,0,0,5"/>
|
|
||||||
<TextBox x:Name="EmailTextBox" Height="32" Padding="8,0" Margin="0,0,0,15"
|
|
||||||
BorderBrush="#BDBDBD" BorderThickness="1"/>
|
|
||||||
|
|
||||||
<!-- 年龄 -->
|
|
||||||
<TextBlock Text="年龄 *" FontWeight="Bold" Margin="0,0,0,5"/>
|
|
||||||
<TextBox x:Name="AgeTextBox" Height="32" Padding="8,0" Margin="0,0,0,15"
|
|
||||||
BorderBrush="#BDBDBD" BorderThickness="1"/>
|
|
||||||
|
|
||||||
<!-- 激活状态 -->
|
|
||||||
<CheckBox x:Name="IsActiveCheckBox" Content="激活" IsChecked="True" Margin="0,0,0,15"/>
|
|
||||||
|
|
||||||
<!-- 备注 -->
|
|
||||||
<TextBlock Text="备注" FontWeight="Bold" Margin="0,0,0,5"/>
|
|
||||||
<TextBox x:Name="RemarksTextBox" Height="80" Padding="8" Margin="0,0,0,15"
|
|
||||||
TextWrapping="Wrap" AcceptsReturn="True"
|
|
||||||
BorderBrush="#BDBDBD" BorderThickness="1"
|
|
||||||
VerticalScrollBarVisibility="Auto"/>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<!-- 按钮 -->
|
|
||||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
|
|
||||||
<Button Content="保存" Width="100" Height="35" Margin="0,0,10,0" Click="SaveButton_Click"
|
|
||||||
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
|
||||||
<Button Content="取消" Width="100" Height="35" Click="CancelButton_Click"
|
|
||||||
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using Server.Data;
|
|
||||||
|
|
||||||
namespace Server.Views;
|
|
||||||
|
|
||||||
public partial class UserEditDialog : Window
|
|
||||||
{
|
|
||||||
public User UserData { get; private set; }
|
|
||||||
private readonly bool _isEditMode;
|
|
||||||
|
|
||||||
public UserEditDialog(User? existingUser = null)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
_isEditMode = existingUser != null;
|
|
||||||
Title = _isEditMode ? "编辑用户" : "新增用户";
|
|
||||||
|
|
||||||
if (existingUser != null)
|
|
||||||
{
|
|
||||||
UserData = existingUser;
|
|
||||||
LoadUserData(existingUser);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UserData = new User
|
|
||||||
{
|
|
||||||
IsActive = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadUserData(User user)
|
|
||||||
{
|
|
||||||
UserNameTextBox.Text = user.UserName;
|
|
||||||
EmailTextBox.Text = user.Email;
|
|
||||||
AgeTextBox.Text = user.Age.ToString();
|
|
||||||
IsActiveCheckBox.IsChecked = user.IsActive;
|
|
||||||
RemarksTextBox.Text = user.Remarks;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
// 验证
|
|
||||||
if (string.IsNullOrWhiteSpace(UserNameTextBox.Text))
|
|
||||||
{
|
|
||||||
MessageBox.Show("请输入用户名", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
UserNameTextBox.Focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(EmailTextBox.Text))
|
|
||||||
{
|
|
||||||
MessageBox.Show("请输入邮箱", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
EmailTextBox.Focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!int.TryParse(AgeTextBox.Text, out int age) || age < 0 || age > 150)
|
|
||||||
{
|
|
||||||
MessageBox.Show("请输入有效的年龄(0-150)", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
AgeTextBox.Focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简单的邮箱格式验证
|
|
||||||
if (!EmailTextBox.Text.Contains("@"))
|
|
||||||
{
|
|
||||||
MessageBox.Show("请输入有效的邮箱地址", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
EmailTextBox.Focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据
|
|
||||||
UserData.UserName = UserNameTextBox.Text.Trim();
|
|
||||||
UserData.Email = EmailTextBox.Text.Trim();
|
|
||||||
UserData.Age = age;
|
|
||||||
UserData.IsActive = IsActiveCheckBox.IsChecked == true;
|
|
||||||
UserData.Remarks = RemarksTextBox.Text.Trim();
|
|
||||||
|
|
||||||
DialogResult = true;
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
DialogResult = false;
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Server.Views.UserManagementWindow"
|
|
||||||
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:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="用户数据管理 - Sharp.Data 示例" Height="700" Width="1200"
|
|
||||||
WindowStartupLocation="CenterScreen">
|
|
||||||
<Grid Margin="15">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 标题栏 -->
|
|
||||||
<Border Grid.Row="0" Background="#2196F3" Padding="15" CornerRadius="5" Margin="0,0,0,15">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="用户数据管理" FontSize="20" FontWeight="Bold" Foreground="White"/>
|
|
||||||
<TextBlock Text="Sharp.Data 模块集成示例 - SQLite 数据库 + 软删除" FontSize="12" Foreground="White" Opacity="0.9"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 搜索和操作栏 -->
|
|
||||||
<Border Grid.Row="1" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="15" Margin="0,0,0,15">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0,0,15,0">
|
|
||||||
<TextBlock Text="搜索:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
|
||||||
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
Width="250" Height="32" VerticalContentAlignment="Center"
|
|
||||||
Padding="8,0" BorderBrush="#BDBDBD" BorderThickness="1"/>
|
|
||||||
<Button Content="搜索" Width="80" Height="32" Margin="10,0,0,0"
|
|
||||||
Command="{Binding SearchCommand}"
|
|
||||||
Style="{StaticResource ButtonPrimary}"/>
|
|
||||||
<Button Content="清空" Width="80" Height="32" Margin="5,0,0,0"
|
|
||||||
Command="{Binding ClearSearchCommand}"
|
|
||||||
Style="{StaticResource ButtonDefault}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- 状态信息 -->
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding StatusText}" VerticalAlignment="Center"
|
|
||||||
Foreground="#666" Margin="15,0"/>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
|
||||||
<Button Content="➕ 新增" Width="90" Height="32" Margin="0,0,5,0"
|
|
||||||
Command="{Binding AddCommand}"
|
|
||||||
Style="{StaticResource ButtonSuccess}"/>
|
|
||||||
<Button Content="✏️ 编辑" Width="90" Height="32" Margin="0,0,5,0"
|
|
||||||
Command="{Binding EditCommand}"
|
|
||||||
Style="{StaticResource ButtonWarning}"/>
|
|
||||||
<Button Content="🗑️ 删除" Width="90" Height="32"
|
|
||||||
Command="{Binding DeleteCommand}"
|
|
||||||
Style="{StaticResource ButtonDanger}"/>
|
|
||||||
<Button Content="🔄 刷新" Width="90" Height="32" Margin="5,0,0,0"
|
|
||||||
Command="{Binding RefreshCommand}"
|
|
||||||
Style="{StaticResource ButtonInfo}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 数据列表 -->
|
|
||||||
<Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="10">
|
|
||||||
<DataGrid ItemsSource="{Binding Users}"
|
|
||||||
SelectedItem="{Binding SelectedUser}"
|
|
||||||
AutoGenerateColumns="False"
|
|
||||||
IsReadOnly="True"
|
|
||||||
SelectionMode="Single"
|
|
||||||
GridLinesVisibility="None"
|
|
||||||
HeadersVisibility="Column"
|
|
||||||
AlternatingRowBackground="#F5F5F5"
|
|
||||||
RowHeight="40"
|
|
||||||
CanUserAddRows="False"
|
|
||||||
CanUserDeleteRows="False">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="80" Visibility="Collapsed"/>
|
|
||||||
<DataGridTextColumn Header="用户名" Binding="{Binding UserName}" Width="150"/>
|
|
||||||
<DataGridTextColumn Header="邮箱" Binding="{Binding Email}" Width="200"/>
|
|
||||||
<DataGridTextColumn Header="年龄" Binding="{Binding Age}" Width="80"/>
|
|
||||||
<DataGridCheckBoxColumn Header="激活" Binding="{Binding IsActive}" Width="80"/>
|
|
||||||
<DataGridTextColumn Header="备注" Binding="{Binding Remarks}" Width="*"/>
|
|
||||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreatedAt, StringFormat={}{0:yyyy-MM-dd HH:mm}}" Width="150"/>
|
|
||||||
<DataGridTextColumn Header="创建人" Binding="{Binding CreatedBy}" Width="100"/>
|
|
||||||
<DataGridTextColumn Header="修改时间" Binding="{Binding UpdatedAt, StringFormat={}{0:yyyy-MM-dd HH:mm}}" Width="150"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 底部信息栏 -->
|
|
||||||
<Border Grid.Row="3" Background="#E3F2FD" Padding="15" CornerRadius="5" Margin="0,15,0,0">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock FontWeight="Bold" Margin="0,0,0,5">
|
|
||||||
<Run Text="💡 功能说明:"/>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock FontSize="11" TextWrapping="Wrap">
|
|
||||||
<Run Text="• 本示例展示 Sharp.Data 模块的使用:SQLite 数据库、仓储模式、自动审计、软删除"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• 删除操作使用软删除(IsDeleted=true),数据不会真正删除"/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Run Text="• 创建/修改时间和操作人由 Data 模块自动记录"/>
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="数据库: SQLite (server.db)" FontSize="11" FontWeight="Bold"/>
|
|
||||||
<TextBlock Text="{Binding RecordCount}" FontSize="11"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using HandyControl.Controls;
|
|
||||||
using Server.ViewModels;
|
|
||||||
|
|
||||||
namespace Server.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for UserManagementWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class UserManagementWindow : GlowWindow
|
|
||||||
{
|
|
||||||
public UserManagementWindow(UserManagementViewModel viewModel)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"Server": {
|
|
||||||
"Port": 8080,
|
|
||||||
"Host": "localhost",
|
|
||||||
"MaxConcurrentRequests": 100
|
|
||||||
},
|
|
||||||
"SharpLog": {
|
|
||||||
"UseConsole": true,
|
|
||||||
"MinLevel": "Information",
|
|
||||||
"File": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Path": "logs/server-.log",
|
|
||||||
"Rolling": "Day",
|
|
||||||
"RetainedFileCountLimit": 30,
|
|
||||||
"Json": true
|
|
||||||
},
|
|
||||||
"Enrich": {
|
|
||||||
"Application": "SharpServer",
|
|
||||||
"WithEnvironmentName": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user