A:调整页面展示和引入方式

This commit is contained in:
jimmy committed 2025-12-16 15:00:53 +08:00
1 parent 4ec76b7855
commit dd0681ba90
10 files changed
+786 -517

No files matched your search

+7 -3
View File
@@ -1,9 +1,13 @@
<Application x:Class="Server.App" <Application x:Class="Server.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Server" xmlns:local="clr-namespace:Server">
StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>
+120 -83
View File
@@ -1,12 +1,19 @@
using System.Windows; using System;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Config; using Config;
using Sharp.Log; using Sharp.Log.Extensions;
using Sharp.Data; using Sharp.Data;
using Sharp.Data.Context; using Sharp.Data.Context;
using Sharp.Wpf.Services;
using Sharp.Wpf.Abstractions;
using Server.Config; using Server.Config;
using Server.Services; using Server.Services;
using Server.Data; using Server.Data;
using Server.ViewModels;
using Server.Views;
namespace Server namespace Server
{ {
@@ -15,32 +22,124 @@ namespace Server
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
public ConfigManager? ConfigManager { get; private set; } private ServiceProvider? _serviceProvider;
public AppConfig? AppConfig { get; private set; }
public ILoggerFactory? LoggerFactory { get; private set; }
public ServerService? ServerService { get; private set; }
public IDataContext? DataContext { get; private set; }
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
base.OnStartup(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 try
{ {
// 1. 初始化配置 // 2. 构建配置
InitializeConfiguration(); var cm = ConfigDefaults.BuildDefault(defaults =>
{
defaults.MainFile = "appsettings.json";
defaults.IncludeEnvironmentVariables = true;
}, configure =>
{
configure.ReloadOnChange = true;
});
// 2. 初始化日志系统 // 3. 构建依赖注入容器
InitializeLogging(); var services = new ServiceCollection();
// 3. 初始化数据库 // 配置管理
InitializeDatabase(); services.AddSingleton(cm);
services.AddSingleton<IConfiguration>(cm.Configuration);
var appConfig = cm.Get<AppConfig>(validate: true);
services.AddSingleton(appConfig);
services.AddSingleton(appConfig.Server);
// 4. 初始化服务器服务 // 日志
InitializeServerService(); services.AddLogging(b => b.AddSharpLog());
var logger = LoggerFactory?.CreateLogger<App>(); // 数据库
logger?.LogInformation("应用程序启动成功"); 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.AddSingleton<IDialogService, DialogService>();
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) catch (Exception ex)
{ {
@@ -54,75 +153,14 @@ namespace Server
} }
} }
private void InitializeConfiguration()
{
ConfigManager = ConfigDefaults.BuildDefault();
AppConfig = ConfigManager.Get<AppConfig>(validate: true);
}
private void InitializeLogging()
{
if (ConfigManager == null)
{
throw new InvalidOperationException("配置未初始化");
}
LoggerFactory = SharpLogDefaults.CreateLoggerFactory(
configuration: ConfigManager.Configuration,
configure: options =>
{
options.Enrich.Application ??= "SharpServer";
});
}
private void InitializeDatabase()
{
if (LoggerFactory == null)
{
throw new InvalidOperationException("日志系统未初始化");
}
var logger = LoggerFactory.CreateLogger<App>();
logger.LogInformation("正在初始化数据库...");
try
{
var builder = new DataContextBuilder<AppDbContext>();
builder.UseSqlite("Data Source=server.db")
.UseAudit(true)
.UseSoftDelete(true)
.UseCurrentUser("system")
.EnableDetailedErrors(true);
DataContext = builder.Build();
logger.LogInformation("数据库初始化成功");
}
catch (Exception ex)
{
logger.LogError(ex, "数据库初始化失败");
throw;
}
}
private void InitializeServerService()
{
if (AppConfig == null || LoggerFactory == null)
{
throw new InvalidOperationException("配置或日志系统未初始化");
}
ServerService = new ServerService(AppConfig, LoggerFactory);
}
protected override void OnExit(ExitEventArgs e) protected override void OnExit(ExitEventArgs e)
{ {
try try
{ {
ServerService?.Dispose(); _serviceProvider?.GetService<ServerService>()?.Dispose();
DataContext?.Dispose(); _serviceProvider?.GetService<IDataContext>()?.Dispose();
var logger = LoggerFactory?.CreateLogger<App>(); var logger = _serviceProvider?.GetService<ILogger<App>>();
logger?.LogInformation("应用程序正常退出"); logger?.LogInformation("应用程序正常退出");
} }
catch (Exception ex) catch (Exception ex)
@@ -132,11 +170,10 @@ namespace Server
} }
finally finally
{ {
LoggerFactory?.Dispose(); _serviceProvider?.Dispose();
} }
base.OnExit(e); base.OnExit(e);
} }
} }
} }
+25 -17
View File
@@ -1,9 +1,10 @@
<Window x:Class="Server.MainWindow" <hc:GlowWindow x:Class="Server.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Server" xmlns:local="clr-namespace:Server"
xmlns:hc="https://handyorg.github.io/handycontrol"
mc:Ignorable="d" mc:Ignorable="d"
Title="Sharp HTTP Server" Height="600" Width="900" Title="Sharp HTTP Server" Height="600" Width="900"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
@@ -38,25 +39,33 @@
<!-- 服务器信息 --> <!-- 服务器信息 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="服务器地址:" VerticalAlignment="Center" Margin="0,0,10,5"/> <TextBlock Grid.Row="0" Grid.Column="0" Text="服务器地址:" VerticalAlignment="Center" Margin="0,0,10,5"/>
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="ServerUrlTextBlock" Text="http://localhost:8080" <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ServerUrl}"
FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/> 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="0" Text="服务器状态:" VerticalAlignment="Center" Margin="0,0,10,5"/>
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="StatusTextBlock" Text="未启动" <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding StatusText}"
Foreground="Gray" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/> 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"> <StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,10,0,0">
<Button x:Name="StartButton" Content="启动服务器" Width="100" Height="32" Margin="0,0,10,0" Click="StartButton_Click" <Button Content="➕ 启动服务器" Width="120" Height="32" Margin="0,0,10,0"
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> Command="{Binding StartCommand}"
<Button x:Name="StopButton" Content="停止服务器" Width="100" Height="32" Margin="0,0,10,0" Click="StopButton_Click" IsEnabled="{Binding CanStart}"
Background="#F44336" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/> Style="{StaticResource ButtonSuccess}"/>
<Button x:Name="RestartButton" Content="重启服务器" Width="100" Height="32" Margin="0,0,10,0" Click="RestartButton_Click" <Button Content="⏹ 停止服务器" Width="120" Height="32" Margin="0,0,10,0"
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/> Command="{Binding StopCommand}"
<Button x:Name="DataManagementButton" Content="数据管理" Width="100" Height="32" Margin="0,0,10,0" Click="DataManagementButton_Click" IsEnabled="{Binding IsRunning}"
Background="#673AB7" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> Style="{StaticResource ButtonDanger}"/>
<Button x:Name="ClearLogButton" Content="清空日志" Width="100" Height="32" Click="ClearLogButton_Click" <Button Content="🔄 重启服务器" Width="120" Height="32" Margin="0,0,10,0"
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> 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> </StackPanel>
</Grid> </Grid>
</Border> </Border>
@@ -72,7 +81,7 @@
<TextBlock Grid.Row="0" Text="日志信息" FontSize="14" FontWeight="Bold" Margin="0,0,0,10"/> <TextBlock Grid.Row="0" Text="日志信息" FontSize="14" FontWeight="Bold" Margin="0,0,0,10"/>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<TextBox x:Name="LogTextBox" <TextBox Text="{Binding LogText, Mode=OneWay}"
IsReadOnly="True" IsReadOnly="True"
TextWrapping="Wrap" TextWrapping="Wrap"
Background="#F5F5F5" Background="#F5F5F5"
@@ -110,5 +119,4 @@
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>
</Window> </hc:GlowWindow>
+10 -189
View File
@@ -1,203 +1,24 @@
using System.Windows; using HandyControl.Controls;
using Server.Services; using Server.ViewModels;
using Server.Config;
using Server.Views;
using Microsoft.Extensions.Logging;
namespace Server namespace Server
{ {
/// <summary> /// <summary>
/// Interaction logic for MainWindow.xaml /// Interaction logic for MainWindow.xaml
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : GlowWindow
{ {
private ServerService? _serverService; public MainWindow(MainViewModel viewModel)
private AppConfig? _config;
private ILogger<MainWindow>? _logger;
public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
Loaded += MainWindow_Loaded; DataContext = viewModel;
Closing += MainWindow_Closing;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e) // Handle window closing event
{ Closing += (s, e) =>
try
{ {
// 从App获取依赖 // Check if we need to stop the server before closing
var app = (App)Application.Current; e.Cancel = !viewModel.CheckBeforeClose();
_config = app.AppConfig; };
_logger = app.LoggerFactory?.CreateLogger<MainWindow>();
_serverService = app.ServerService;
if (_config != null && _serverService != null)
{
// 更新UI显示服务器地址
ServerUrlTextBlock.Text = $"http://{_config.Server.Host}:{_config.Server.Port}";
// 订阅服务器状态变化事件
_serverService.StatusChanged += OnServerStatusChanged;
AppendLog("应用程序已启动");
AppendLog($"配置已加载: {_config.Server.Host}:{_config.Server.Port}");
}
else
{
AppendLog("错误: 无法初始化服务器服务");
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "MainWindow初始化失败");
AppendLog($"初始化失败: {ex.Message}");
}
}
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
try
{
if (_serverService?.IsRunning == true)
{
var result = MessageBox.Show(
"服务器正在运行,是否停止服务器并退出?",
"确认退出",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
_serverService.Stop();
AppendLog("服务器已停止");
}
else
{
e.Cancel = true;
}
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "关闭窗口时出错");
AppendLog($"关闭出错: {ex.Message}");
}
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
try
{
_serverService?.Start();
UpdateButtonStates(isRunning: true);
}
catch (Exception ex)
{
_logger?.LogError(ex, "启动服务器失败");
AppendLog($"启动失败: {ex.Message}");
MessageBox.Show($"启动服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void StopButton_Click(object sender, RoutedEventArgs e)
{
try
{
_serverService?.Stop();
UpdateButtonStates(isRunning: false);
}
catch (Exception ex)
{
_logger?.LogError(ex, "停止服务器失败");
AppendLog($"停止失败: {ex.Message}");
MessageBox.Show($"停止服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void RestartButton_Click(object sender, RoutedEventArgs e)
{
try
{
_serverService?.Restart();
UpdateButtonStates(isRunning: true);
}
catch (Exception ex)
{
_logger?.LogError(ex, "重启服务器失败");
AppendLog($"重启失败: {ex.Message}");
MessageBox.Show($"重启服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ClearLogButton_Click(object sender, RoutedEventArgs e)
{
LogTextBox.Clear();
}
private void DataManagementButton_Click(object sender, RoutedEventArgs e)
{
try
{
_logger?.LogInformation("打开数据管理窗口");
var window = new UserManagementWindow
{
Owner = this
};
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)
{
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = status;
// 根据状态更新颜色
if (status.Contains("已启动"))
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Green;
}
else if (status.Contains("已停止"))
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Gray;
}
else if (status.Contains("失败") || status.Contains("错误"))
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Red;
}
else
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Orange;
}
AppendLog(status);
});
}
private void UpdateButtonStates(bool isRunning)
{
StartButton.IsEnabled = !isRunning;
StopButton.IsEnabled = isRunning;
RestartButton.IsEnabled = isRunning;
}
private void AppendLog(string message)
{
Dispatcher.Invoke(() =>
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
LogTextBox.AppendText($"[{timestamp}] {message}\n");
LogTextBox.ScrollToEnd();
});
} }
} }
} }
+1
View File
@@ -28,6 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="HandyControl" Version="3.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -0,0 +1,278 @@
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();
}
}
}
@@ -0,0 +1,308 @@
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,8 +1,9 @@
<Window x:Class="Server.Views.UserManagementWindow" <hc:GlowWindow x:Class="Server.Views.UserManagementWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
mc:Ignorable="d" mc:Ignorable="d"
Title="用户数据管理 - Sharp.Data 示例" Height="700" Width="1200" Title="用户数据管理 - Sharp.Data 示例" Height="700" Width="1200"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
@@ -34,35 +35,43 @@
<!-- 搜索框 --> <!-- 搜索框 -->
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0,0,15,0"> <StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0,0,15,0">
<TextBlock Text="搜索:" VerticalAlignment="Center" Margin="0,0,10,0"/> <TextBlock Text="搜索:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox x:Name="SearchTextBox" Width="250" Height="32" VerticalContentAlignment="Center" <TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Width="250" Height="32" VerticalContentAlignment="Center"
Padding="8,0" BorderBrush="#BDBDBD" BorderThickness="1"/> Padding="8,0" BorderBrush="#BDBDBD" BorderThickness="1"/>
<Button Content="搜索" Width="80" Height="32" Margin="10,0,0,0" Click="SearchButton_Click" <Button Content="搜索" Width="80" Height="32" Margin="10,0,0,0"
Background="#2196F3" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> Command="{Binding SearchCommand}"
<Button Content="清空" Width="80" Height="32" Margin="5,0,0,0" Click="ClearSearchButton_Click" Style="{StaticResource ButtonPrimary}"/>
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> <Button Content="清空" Width="80" Height="32" Margin="5,0,0,0"
Command="{Binding ClearSearchCommand}"
Style="{StaticResource ButtonDefault}"/>
</StackPanel> </StackPanel>
<!-- 状态信息 --> <!-- 状态信息 -->
<TextBlock Grid.Column="1" x:Name="StatusTextBlock" Text="就绪" VerticalAlignment="Center" <TextBlock Grid.Column="1" Text="{Binding StatusText}" VerticalAlignment="Center"
Foreground="#666" Margin="15,0"/> Foreground="#666" Margin="15,0"/>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<StackPanel Grid.Column="2" Orientation="Horizontal"> <StackPanel Grid.Column="2" Orientation="Horizontal">
<Button Content="➕ 新增" Width="90" Height="32" Margin="0,0,5,0" Click="AddButton_Click" <Button Content="➕ 新增" Width="90" Height="32" Margin="0,0,5,0"
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> Command="{Binding AddCommand}"
<Button Content="✏️ 编辑" Width="90" Height="32" Margin="0,0,5,0" Click="EditButton_Click" Style="{StaticResource ButtonSuccess}"/>
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> <Button Content="✏️ 编辑" Width="90" Height="32" Margin="0,0,5,0"
<Button Content="🗑️ 删除" Width="90" Height="32" Click="DeleteButton_Click" Command="{Binding EditCommand}"
Background="#F44336" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> Style="{StaticResource ButtonWarning}"/>
<Button Content="🔄 刷新" Width="90" Height="32" Margin="5,0,0,0" Click="RefreshButton_Click" <Button Content="🗑️ 删除" Width="90" Height="32"
Background="#607D8B" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/> Command="{Binding DeleteCommand}"
Style="{StaticResource ButtonDanger}"/>
<Button Content="🔄 刷新" Width="90" Height="32" Margin="5,0,0,0"
Command="{Binding RefreshCommand}"
Style="{StaticResource ButtonInfo}"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
<!-- 数据列表 --> <!-- 数据列表 -->
<Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="10"> <Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="10">
<DataGrid x:Name="UsersDataGrid" <DataGrid ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser}"
AutoGenerateColumns="False" AutoGenerateColumns="False"
IsReadOnly="True" IsReadOnly="True"
SelectionMode="Single" SelectionMode="Single"
@@ -109,9 +118,9 @@
<StackPanel Grid.Column="1" VerticalAlignment="Center"> <StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="数据库: SQLite (server.db)" FontSize="11" FontWeight="Bold"/> <TextBlock Text="数据库: SQLite (server.db)" FontSize="11" FontWeight="Bold"/>
<TextBlock x:Name="RecordCountTextBlock" Text="记录数: 0" FontSize="11"/> <TextBlock Text="{Binding RecordCount}" FontSize="11"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
</Window> </hc:GlowWindow>
@@ -1,202 +1,17 @@
using System.Windows; using HandyControl.Controls;
using System.Windows.Controls; using Server.ViewModels;
using Sharp.Data.Context;
using Server.Data;
using Common.ExpandFun;
using Microsoft.Extensions.Logging;
namespace Server.Views; namespace Server.Views
public partial class UserManagementWindow : Window
{ {
private readonly IDataContext _dataContext; /// <summary>
private readonly ILogger<UserManagementWindow> _logger; /// Interaction logic for UserManagementWindow.xaml
/// </summary>
public UserManagementWindow() public partial class UserManagementWindow : GlowWindow
{ {
InitializeComponent(); public UserManagementWindow(UserManagementViewModel viewModel)
var app = (App)Application.Current;
_dataContext = app.DataContext ?? throw new InvalidOperationException("数据库未初始化");
_logger = app.LoggerFactory?.CreateLogger<UserManagementWindow>()
?? throw new InvalidOperationException("日志系统未初始化");
Loaded += UserManagementWindow_Loaded;
}
private async void UserManagementWindow_Loaded(object sender, RoutedEventArgs e)
{
await LoadDataAsync();
}
private async Task LoadDataAsync(string? searchText = null)
{
try
{ {
StatusTextBlock.Text = "正在加载数据..."; InitializeComponent();
_logger.LogInformation("开始加载用户数据,搜索条件: {SearchText}", searchText ?? "无"); DataContext = viewModel;
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)));
}
UsersDataGrid.ItemsSource = users.OrderByDescending(u => u.CreatedAt).ToList();
RecordCountTextBlock.Text = $"记录数: {users.Count}";
StatusTextBlock.Text = string.IsNullOrWhiteSpace(searchText)
? $"加载完成,共 {users.Count} 条记录"
: $"搜索完成,找到 {users.Count} 条记录";
_logger.LogInformation("加载完成,共 {Count} 条记录", users.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "加载数据失败");
StatusTextBlock.Text = "加载失败";
MessageBox.Show($"加载数据失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void SearchButton_Click(object sender, RoutedEventArgs e)
{
var searchText = SearchTextBox.Text.Trim();
await LoadDataAsync(searchText);
}
private async void ClearSearchButton_Click(object sender, RoutedEventArgs e)
{
SearchTextBox.Clear();
await LoadDataAsync();
}
private void AddButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new UserEditDialog();
if (dialog.ShowDialog() == true)
{
_ = SaveUserAsync(dialog.UserData);
}
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
if (UsersDataGrid.SelectedItem is not User selectedUser)
{
MessageBox.Show("请选择要编辑的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var dialog = new UserEditDialog(selectedUser);
if (dialog.ShowDialog() == true)
{
_ = UpdateUserAsync(dialog.UserData);
}
}
private async void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (UsersDataGrid.SelectedItem is not User selectedUser)
{
MessageBox.Show("请选择要删除的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var result = MessageBox.Show(
$"确定要删除用户 '{selectedUser.UserName}' 吗?\n\n(注意:这是软删除,数据会被标记为已删除但不会真正删除)",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
await DeleteUserAsync(selectedUser.Id);
}
}
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
await LoadDataAsync(SearchTextBox.Text.Trim());
}
private async Task SaveUserAsync(User user)
{
try
{
StatusTextBlock.Text = "正在保存...";
_logger.LogInformation("新增用户: {UserName}", user.UserName);
var repo = _dataContext.Repository<User>();
await repo.AddAsync(user);
await _dataContext.SaveChangesAsync();
_logger.LogInformation("用户新增成功: {UserId}", user.Id);
MessageBox.Show("用户新增成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
await LoadDataAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "新增用户失败");
StatusTextBlock.Text = "保存失败";
MessageBox.Show($"新增用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task UpdateUserAsync(User user)
{
try
{
StatusTextBlock.Text = "正在更新...";
_logger.LogInformation("更新用户: {UserId}", user.Id);
var repo = _dataContext.Repository<User>();
repo.Update(user);
await _dataContext.SaveChangesAsync();
_logger.LogInformation("用户更新成功: {UserId}", user.Id);
MessageBox.Show("用户更新成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
await LoadDataAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "更新用户失败");
StatusTextBlock.Text = "更新失败";
MessageBox.Show($"更新用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task DeleteUserAsync(Guid userId)
{
try
{
StatusTextBlock.Text = "正在删除...";
_logger.LogInformation("软删除用户: {UserId}", userId);
var repo = _dataContext.Repository<User>();
await repo.RemoveAsync(userId); // 软删除
await _dataContext.SaveChangesAsync();
_logger.LogInformation("用户软删除成功: {UserId}", userId);
MessageBox.Show("用户已标记为删除(软删除)!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
await LoadDataAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "删除用户失败");
StatusTextBlock.Text = "删除失败";
MessageBox.Show($"删除用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
} }
-12
View File
@@ -111,15 +111,3 @@ var folder = await new FileDialogService().SelectFolderAsync();
- 若要禁用全局异常弹窗,可在 `ExceptionHandlingOptions` 中设置 `ShowDialog = false` 并仅使用 `Log` 钩子记录。 - 若要禁用全局异常弹窗,可在 `ExceptionHandlingOptions` 中设置 `ShowDialog = false` 并仅使用 `Log` 钩子记录。
- `EventToCommand` 通过反射订阅事件,适用于大多数 Routed 事件;如需传递事件参数,可设置 `PassEventArgsToCommand="True"`。 - `EventToCommand` 通过反射订阅事件,适用于大多数 Routed 事件;如需传递事件参数,可设置 `PassEventArgsToCommand="True"`。
我们主要使用Data模块,Repository模块不是非常常用。增加你提到的功能,并在Client模块提供对应功能测试、使用案例,使用Sqlite数据库作为测试数据库。
对于NetServer增加JWT验证选项(可选使用),增加对于WebSocket的支持、API版本支持、健康端点检查、模型验证等,并且Server模块增加对应功能的测试、使用案例,在Client进行连接通信
对于PLC模块增加Modbus的支持,并在Server中提供使用示例。
Ports 模块增加ModbusRTU功能的支持、帧同步器、虚拟串口,并且在Client中增加对应功能的演示
这么多模块和功能,他们分别是客户端和服务端的功能,能不能让Client和Server集成他们需要集成的内容,尽可能的内容复用,各个模块之间也进行引用减少重复代码。从这个角度审视目前的所有的代码,并重新设计并在Client和Server中进行集成。并且
Client和Server页面也使用HandyControl