完成显示日志功能

This commit is contained in:
2025-09-07 19:48:48 +08:00
parent 0e4a306fa7
commit 5f0a4b23f4
15 changed files with 425 additions and 68 deletions

View File

@@ -23,6 +23,11 @@ public interface INlogAppService
/// <param name="count">要获取的日志条目数量。</param>
Task<List<NlogDto>> GetLatestLogsAsync(int count);
/// <summary>
/// 异步清空所有Nlog日志。
/// </summary>
Task ClearAllLogsAsync();
// 可以在这里添加更多针对日志的查询服务方法,例如:
// Task<List<NlogDto>> GetLogsByLevelAsync(string level);
// Task<List<NlogDto>> GetLogsByDateRangeAsync(DateTime startDate, DateTime endDate);

View File

@@ -47,5 +47,6 @@ public class MappingProfile : Profile
// User 映射
CreateMap<User, UserDto>().ReverseMap();
CreateMap<Nlog, NlogDto>().ReverseMap();
}
}

View File

@@ -65,6 +65,17 @@ public class NlogAppService : INlogAppService
return allLogs.OrderByDescending(l => l.LogTime).Take(count).ToList();
}
/// <summary>
/// 异步清空所有Nlog日志。
/// </summary>
public async Task ClearAllLogsAsync()
{
// 这里需要实现清空日志的逻辑
// 暂时只清空UI上的日志列表
// 实际实现可能需要在 INlogRepository 中添加相应的方法
await _repoManager.Nlogs.DeleteAllAsync();
}
// 可以在这里实现 INlogAppService 接口中定义的其他方法
// 例如:
/*

View File

@@ -8,6 +8,11 @@ namespace DMS.Core.Interfaces.Repositories;
/// </summary>
public interface INlogRepository : IBaseRepository<Nlog>
{
/// <summary>
/// 异步删除所有Nlog日志。
/// </summary>
Task DeleteAllAsync();
// 可以在此处添加 Nlog 特定的查询方法,例如:
// Task<List<Nlog>> GetLogsByLevelAsync(string level);
// Task<List<Nlog>> GetLogsByDateRangeAsync(DateTime startDate, DateTime endDate);

View File

@@ -34,5 +34,6 @@ public class MappingProfile : Profile
CreateMap<DbMenu, MenuBean>().ReverseMap();
CreateMap<DbNlog, Nlog>().ReverseMap();
}
}

View File

@@ -134,14 +134,20 @@ public class InitializeRepository : IInitializeRepository
},
new DbMenu
{
Id = 5, Header = "设置", Icon = "\uE713", ParentId = 0,
MenuType = MenuType.MainMenu, TargetViewKey = "SettingView",
Id = 5, Header = "日志历史", Icon = "\uE7BA", ParentId = 0,
MenuType = MenuType.MainMenu, TargetViewKey = "LogHistoryView",
DisplayOrder = 5
},
new DbMenu
{
Id = 6, Header = "关于", Icon = "\uE946", ParentId = 0,
MenuType = MenuType.MainMenu, TargetViewKey = "", DisplayOrder = 6
Id = 6, Header = "设置", Icon = "\uE713", ParentId = 0,
MenuType = MenuType.MainMenu, TargetViewKey = "SettingView",
DisplayOrder = 6
},
new DbMenu
{
Id = 7, Header = "关于", Icon = "\uE946", ParentId = 0,
MenuType = MenuType.MainMenu, TargetViewKey = "", DisplayOrder = 7
} // 假设有一个AboutView
};

View File

@@ -27,6 +27,14 @@ public class NlogRepository : BaseRepository<DbNlog>, INlogRepository
_mapper = mapper;
}
/// <summary>
/// 异步删除所有Nlog日志。
/// </summary>
public async Task DeleteAllAsync()
{
await Db.Deleteable<DbNlog>().ExecuteCommandAsync();
}
// Nlog 通常是只读或追加的日志,因此像 AddAsync, UpdateAsync, DeleteAsync 这样的修改方法
// 可能不需要在仓储接口中暴露,或者可以省略具体实现或抛出 NotSupportedException。
// 但为了保持与基类一致性并满足接口要求,这里显式实现它们。

View File

@@ -221,6 +221,7 @@ public partial class App : System.Windows.Application
services.AddSingleton<IDataCenterService, DataCenterService>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<INlogAppService, NlogAppService>();
// 注册MQTT服务管理器
services.AddSingleton<IMqttServiceManager, MqttServiceManager>();
@@ -248,6 +249,7 @@ public partial class App : System.Windows.Application
services.AddSingleton<DevicesViewModel>();
services.AddSingleton<DataTransformViewModel>();
services.AddSingleton<SettingViewModel>();
services.AddSingleton<LogHistoryViewModel>();
services.AddTransient<VariableTableViewModel>(provider =>
new VariableTableViewModel(
provider.GetRequiredService<IMapper>(),
@@ -270,6 +272,14 @@ public partial class App : System.Windows.Application
provider.GetRequiredService<INotificationService>()
)
);
services.AddSingleton<LogHistoryViewModel>(provider =>
new LogHistoryViewModel(
provider.GetRequiredService<IMapper>(),
provider.GetRequiredService<INlogAppService>(),
provider.GetRequiredService<IDialogService>(),
provider.GetRequiredService<INotificationService>()
)
);
services.AddScoped<MqttServerDetailViewModel>();
// 注册对话框视图模型
@@ -292,6 +302,7 @@ public partial class App : System.Windows.Application
services.AddSingleton<HomeView>();
services.AddSingleton<DevicesView>();
services.AddSingleton<VariableTableView>();
services.AddSingleton<LogHistoryView>();
services.AddScoped<DeviceDetailView>();
services.AddScoped<MqttsView>();
}

View File

@@ -29,6 +29,7 @@ namespace DMS.WPF.Profiles
CreateMap<VariableDto, VariableItemViewModel>().ReverseMap();
CreateMap<VariableMqttAliasDto, VariableMqttAliasItemViewModel>().ReverseMap();
CreateMap<VariableTableDto, VariableTableItemViewModel>().ReverseMap();
CreateMap<NlogDto, NlogItemViewModel>().ReverseMap();
}
}
}

View File

@@ -11,13 +11,15 @@ namespace DMS.WPF.Services;
public class NavigationService : INavigationService
{
private readonly IServiceProvider _serviceProvider;
private readonly INotificationService _notificationService;
/// <summary>
/// 构造函数。
/// </summary>
public NavigationService(IServiceProvider serviceProvider)
public NavigationService(IServiceProvider serviceProvider,INotificationService notificationService)
{
_serviceProvider = serviceProvider;
_notificationService = notificationService;
}
/// <summary>
@@ -34,8 +36,7 @@ public class NavigationService : INavigationService
var viewModel = GetViewModelByKey(menu.TargetViewKey);
if (viewModel == null)
{
var notificationService = App.Current.Services.GetRequiredService<INotificationService>();
notificationService.ShowError($"切换界面失败,没有找到界面:{menu.TargetViewKey}");
_notificationService.ShowError($"切换界面失败,没有找到界面:{menu.TargetViewKey}");
return;
}
@@ -50,6 +51,8 @@ public class NavigationService : INavigationService
private ViewModelBase GetViewModelByKey(string key)
{
try
{
switch (key)
{
@@ -63,6 +66,8 @@ public class NavigationService : INavigationService
return App.Current.Services.GetRequiredService<DataTransformViewModel>();
case "VariableTableView":
return App.Current.Services.GetRequiredService<VariableTableViewModel>();
case "LogHistoryView":
return App.Current.Services.GetRequiredService<LogHistoryViewModel>();
case "MqttsView":
return App.Current.Services.GetRequiredService<MqttsViewModel>();
case "MqttServerDetailView":
@@ -73,4 +78,10 @@ public class NavigationService : INavigationService
return null;
}
}
catch (Exception e)
{
_notificationService.ShowError($"切换界面失败,获取:{key}对应的ViewModel时发生了错误{e.Message}");
throw;
}
}
}

View File

@@ -1,50 +1,24 @@
// 文件: DMS.WPF/ViewModels/Items/NlogItemViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using DMS.Core.Models;
namespace DMS.WPF.ViewModels.Items;
/// <summary>
/// 代表日志列表中的单个日志项的ViewModel。
/// 实现了INotifyPropertyChanged其任何属性变化都会自动通知UI。
/// </summary>
public partial class NlogItemViewModel : ObservableObject
public class NlogItemViewModel : ObservableObject
{
public int Id { get; set; }
private Nlog _nlog;
[ObservableProperty]
private DateTime _logTime;
public NlogItemViewModel(Nlog nlog)
{
_nlog = nlog;
}
[ObservableProperty]
private string _level;
[ObservableProperty]
private int _threadId;
[ObservableProperty]
private string _threadName;
[ObservableProperty]
private string _callsite;
[ObservableProperty]
private int _callsiteLineNumber;
[ObservableProperty]
private string _message;
[ObservableProperty]
private string _logger;
[ObservableProperty]
private string _exception;
[ObservableProperty]
private string _callerFilePath;
[ObservableProperty]
private int _callerLineNumber;
[ObservableProperty]
private string _callerMember;
public int Id => _nlog.Id;
public string Level => _nlog.Level;
public string ThreadName => _nlog.ThreadName;
public string Callsite => _nlog.Callsite;
public string Message => _nlog.Message;
public string Logger => _nlog.Logger;
public string Exception => _nlog.Exception;
public string StackTrace => _nlog.Exception; // Using Exception as StackTrace since it's not in the Nlog model
public System.DateTime TimeStamp => _nlog.LogTime;
}

View File

@@ -0,0 +1,139 @@
using AutoMapper;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DMS.Application.DTOs;
using DMS.Application.Interfaces;
using DMS.Core.Models;
using DMS.WPF.Interfaces;
using DMS.WPF.ViewModels.Items;
using ObservableCollections;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DMS.WPF.ViewModels.Dialogs;
using Microsoft.Extensions.DependencyInjection;
namespace DMS.WPF.ViewModels;
partial class LogHistoryViewModel : ViewModelBase
{
private readonly IMapper _mapper;
private readonly INlogAppService _nlogAppService;
private readonly IDialogService _dialogService;
private readonly INotificationService _notificationService;
[ObservableProperty]
private NlogItemViewModel _selectedLog;
[ObservableProperty]
private IList _selectedLogs = new ArrayList();
[ObservableProperty]
private string _searchText;
private readonly ObservableList<NlogItemViewModel> _logItemList;
private readonly ISynchronizedView<NlogItemViewModel, NlogItemViewModel> _synchronizedView;
public NotifyCollectionChangedSynchronizedViewList<NlogItemViewModel> LogItemListView { get; }
public LogHistoryViewModel(IMapper mapper, INlogAppService nlogAppService, IDialogService dialogService, INotificationService notificationService)
{
_mapper = mapper;
_nlogAppService = nlogAppService;
_dialogService = dialogService;
_notificationService = notificationService;
_logItemList = new ObservableList<NlogItemViewModel>();
_synchronizedView = _logItemList.CreateView(v => v);
LogItemListView = _synchronizedView.ToNotifyCollectionChanged();
}
private bool FilterLogs(NlogItemViewModel item)
{
var searchTextLower = SearchText.ToLower();
return item.Logger?.ToLower().Contains(searchTextLower) == true ||
item.Message?.ToLower().Contains(searchTextLower) == true ||
item.Exception?.ToLower().Contains(searchTextLower) == true ||
item.StackTrace?.ToLower().Contains(searchTextLower) == true;
}
partial void OnSearchTextChanged(string value)
{
if (string.IsNullOrWhiteSpace(SearchText))
{
_synchronizedView.ResetFilter();
}
else
{
_synchronizedView.AttachFilter(FilterLogs);
}
}
public override async void OnLoaded()
{
await LoadLogsAsync();
}
[RelayCommand]
private async Task RefreshLogsAsync()
{
await LoadLogsAsync();
}
[RelayCommand]
private async Task ClearLogsAsync()
{
var confirmDialogViewModel = new ConfirmDialogViewModel("确认", "确定要清空所有日志吗?", "确定");
var result = await _dialogService.ShowDialogAsync(confirmDialogViewModel);
if (result == true)
{
try
{
await _nlogAppService.ClearAllLogsAsync();
_logItemList.Clear();
_notificationService.ShowInfo("日志已清空");
}
catch (System.Exception ex)
{
_notificationService.ShowError($"清空日志时发生错误: {ex.Message}", ex);
}
}
}
private async Task LoadLogsAsync()
{
try
{
var logs = await _nlogAppService.GetAllLogsAsync();
var logItems = logs.Select(logDto =>
{
// Manually map NlogDto to Nlog
var nlog = new Nlog
{
Id = logDto.Id,
LogTime = logDto.LogTime,
Level = logDto.Level,
ThreadId = logDto.ThreadId,
ThreadName = logDto.ThreadName,
Callsite = logDto.Callsite,
CallsiteLineNumber = logDto.CallsiteLineNumber,
Message = logDto.Message,
Logger = logDto.Logger,
Exception = logDto.Exception,
CallerFilePath = logDto.CallerFilePath,
CallerLineNumber = logDto.CallerLineNumber,
CallerMember = logDto.CallerMember
};
return new NlogItemViewModel(nlog);
}).ToList();
_logItemList.Clear();
_logItemList.AddRange(logItems);
}
catch (System.Exception ex)
{
_notificationService.ShowError($"加载日志时发生错误: {ex.Message}", ex);
}
}
}

View File

@@ -0,0 +1,156 @@
<UserControl
x:Class="DMS.WPF.Views.LogHistoryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ex="clr-namespace:DMS.Extensions"
xmlns:helper="clr-namespace:DMS.WPF.Helper"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:vm="clr-namespace:DMS.WPF.ViewModels"
d:DataContext="{d:DesignInstance vm:LogHistoryViewModel}"
d:DesignHeight="600"
d:DesignWidth="800"
Loaded="LogHistoryView_OnLoaded"
mc:Ignorable="d">
<UserControl.Resources>
<ex:BindingProxy x:Key="proxy" Data="{Binding}" />
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<!-- 标签字体的样式 -->
<Style x:Key="LogHistoryLabelStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- 值字体的样式 -->
<Style x:Key="LogHistoryValueStyle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FontSize" Value="16" />
<Setter Property="MinWidth" Value="100" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</UserControl.Resources>
<DockPanel>
<ikw:SimpleStackPanel Margin="10" DockPanel.Dock="Top">
<!-- 操作菜单 -->
<controls:CommandBar
x:Name="PrimaryCommandBar"
DefaultLabelPosition="Right"
IsOpen="False">
<ui:AppBarButton Command="{Binding RefreshLogsCommand}" Label="刷新日志">
<ui:AppBarButton.Icon>
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.Refresh}" />
</ui:AppBarButton.Icon>
</ui:AppBarButton>
<ui:AppBarButton Command="{Binding ClearLogsCommand}" Label="清空日志">
<ui:AppBarButton.Icon>
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.Delete}" />
</ui:AppBarButton.Icon>
</ui:AppBarButton>
<ui:AppBarButton x:Name="ShareButton" Label="Share">
<ui:AppBarButton.Icon>
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.Share}" />
</ui:AppBarButton.Icon>
</ui:AppBarButton>
<ui:CommandBar.SecondaryCommands>
<ui:AppBarButton
x:Name="SettingsButton"
Icon="Setting"
Label="Settings" />
</ui:CommandBar.SecondaryCommands>
</controls:CommandBar>
<!-- 日志的搜索信息 -->
<ikw:SimpleStackPanel
Margin="5"
Orientation="Horizontal"
Spacing="10">
<TextBlock Style="{StaticResource LogHistoryLabelStyle}" Text="搜索:" />
<TextBox
Width="200"
Margin="5,0,0,0"
HorizontalAlignment="Left"
ui:ControlHelper.PlaceholderText="搜索日志..."
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
</ikw:SimpleStackPanel>
</ikw:SimpleStackPanel>
<DataGrid
x:Name="BasicGridView"
Margin="10"
AutoGenerateColumns="False"
CanUserDeleteRows="False"
CanUserSortColumns="False"
IsReadOnly="True"
ItemsSource="{Binding LogItemListView}"
SelectedItem="{Binding SelectedLog}"
SelectionMode="Extended"
Style="{StaticResource DataGridBaseStyle}">
<i:Interaction.Behaviors>
<helper:SelectedItemsBehavior SelectedItems="{Binding SelectedLogs}" />
</i:Interaction.Behaviors>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding RefreshLogsCommand}" Header="刷新日志">
<MenuItem.Icon>
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.Refresh}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Command="{Binding ClearLogsCommand}" Header="清空日志">
<MenuItem.Icon>
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.Delete}" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<!-- <Setter Property="Background" Value="#fff"/> -->
<Style.Triggers>
<DataTrigger Binding="{Binding Level}" Value="Error">
<Setter Property="Background" Value="LightCoral" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="Bold" />
</DataTrigger>
<DataTrigger Binding="{Binding Level}" Value="Warn">
<Setter Property="Background" Value="LightGoldenrodYellow" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontWeight" Value="Bold" />
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource HoverBrush}" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource PrimaryBrush}" />
<Setter Property="Foreground" Value="{DynamicResource TextIconBrush}" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding TimeStamp, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}" Header="时间戳" />
<DataGridTextColumn Binding="{Binding Level}" Header="级别" />
<DataGridTextColumn Binding="{Binding Logger}" Header="记录器" />
<DataGridTextColumn Binding="{Binding Message}" Header="消息" />
<DataGridTextColumn Binding="{Binding Exception}" Header="异常" />
<DataGridTextColumn Binding="{Binding StackTrace}" Header="堆栈跟踪" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</UserControl>

View File

@@ -0,0 +1,24 @@
using System.Windows;
using System.Windows.Controls;
using DMS.WPF.ViewModels;
namespace DMS.WPF.Views;
/// <summary>
/// LogHistoryView.xaml 的交互逻辑
/// </summary>
public partial class LogHistoryView : UserControl
{
public LogHistoryView()
{
InitializeComponent();
}
private void LogHistoryView_OnLoaded(object sender, RoutedEventArgs e)
{
if (DataContext is LogHistoryViewModel viewModel)
{
viewModel.OnLoaded();
}
}
}

View File

@@ -104,6 +104,10 @@
<DataTemplate DataType="{x:Type vm:SettingViewModel}">
<local:SettingView />
</DataTemplate>
<!-- 日志历史页 -->
<DataTemplate DataType="{x:Type vm:LogHistoryViewModel}">
<local:LogHistoryView />
</DataTemplate>
<!-- 设备详情页 -->
<DataTemplate DataType="{x:Type vm:DeviceDetailViewModel}">
<local:DeviceDetailView />