将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Serilog;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Logging.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// Serilog日志服务实现(适配ILoggerService接口)| Serilog logger service implementation (adapts ILoggerService interface)
|
||||
/// </summary>
|
||||
public class SerilogLoggerService : ILoggerService
|
||||
{
|
||||
private ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数:初始化全局日志实例 | Constructor: initialize global logger instance
|
||||
/// </summary>
|
||||
public SerilogLoggerService()
|
||||
{
|
||||
_logger = Log.Logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 私有构造函数:用于模块标记 | Private constructor: for module tagging
|
||||
/// </summary>
|
||||
private SerilogLoggerService(ILogger logger)
|
||||
{
|
||||
_logger = logger ?? Log.Logger;
|
||||
}
|
||||
|
||||
public void Debug(string message, params object[] args)
|
||||
{
|
||||
_logger.Debug(message, args);
|
||||
}
|
||||
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
_logger.Information(message, args);
|
||||
}
|
||||
|
||||
public void Warn(string message, params object[] args)
|
||||
{
|
||||
_logger.Warning(message, args);
|
||||
}
|
||||
|
||||
public void Error(Exception ex, string message, params object[] args)
|
||||
{
|
||||
_logger.Error(ex, message, args);
|
||||
}
|
||||
|
||||
public void Fatal(Exception ex, string message, params object[] args)
|
||||
{
|
||||
_logger.Fatal(ex, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动指定模块名 | Manually specify module name
|
||||
/// </summary>
|
||||
public ILoggerService ForModule(string moduleName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(moduleName)) return this;
|
||||
return new SerilogLoggerService(_logger.ForContext("SourceContext", moduleName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动使用类型全名作为模块名 | Automatically use type full name as module name
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型参数 | Type parameter</typeparam>
|
||||
public ILoggerService ForModule<T>()
|
||||
{
|
||||
var typeName = typeof(T).FullName ?? typeof(T).Name;
|
||||
return new SerilogLoggerService(_logger.ForContext("SourceContext", typeName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Logging.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用日志服务接口(与具体日志框架解耦)| Generic logger service interface (decoupled from specific logging framework)
|
||||
/// </summary>
|
||||
public interface ILoggerService
|
||||
{
|
||||
/// <summary>
|
||||
/// 调试日志 | Debug log
|
||||
/// </summary>
|
||||
void Debug(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 信息日志 | Information log
|
||||
/// </summary>
|
||||
void Info(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 警告日志 | Warning log
|
||||
/// </summary>
|
||||
void Warn(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 错误日志(带异常)| Error log (with exception)
|
||||
/// </summary>
|
||||
void Error(Exception ex, string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 致命错误日志(带异常)| Fatal error log (with exception)
|
||||
/// </summary>
|
||||
void Fatal(Exception ex, string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 标记日志所属模块(手动指定模块名)| Mark logger module (manually specify module name)
|
||||
/// </summary>
|
||||
/// <param name="moduleName">模块名称 | Module name</param>
|
||||
ILoggerService ForModule(string moduleName);
|
||||
|
||||
/// <summary>
|
||||
/// 标记日志所属模块(自动使用类型全名)| Mark logger module (automatically use type full name)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型参数(自动推断命名空间+类名)| Type parameter (automatically infer namespace + class name)</typeparam>
|
||||
ILoggerService ForModule<T>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Logging.ViewModels;
|
||||
|
||||
namespace XP.Common.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Serilog全局初始化工具(应用启动时调用一次)
|
||||
/// </summary>
|
||||
public static class SerilogInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化Serilog日志
|
||||
/// </summary>
|
||||
public static void Initialize(SerilogConfig config)
|
||||
{
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
// 确保日志目录存在
|
||||
if (!System.IO.Directory.Exists(config.LogPath))
|
||||
System.IO.Directory.CreateDirectory(config.LogPath);
|
||||
|
||||
// 解析日志级别
|
||||
if (!Enum.TryParse<LogEventLevel>(config.MinimumLevel, true, out var minLevel))
|
||||
minLevel = LogEventLevel.Information;
|
||||
|
||||
// 解析文件分割规则
|
||||
if (!Enum.TryParse<RollingInterval>(config.RollingInterval, true, out var rollingInterval))
|
||||
rollingInterval = RollingInterval.Day;
|
||||
|
||||
// 构建Serilog配置
|
||||
var loggerConfig = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(minLevel)
|
||||
.Enrich.FromLogContext() // 启用上下文(模块标记)
|
||||
.WriteTo.File(
|
||||
path: System.IO.Path.Combine(config.LogPath, "app_.log"), // 最终文件名:app_20260307.log
|
||||
rollingInterval: rollingInterval,
|
||||
fileSizeLimitBytes: config.FileSizeLimitMB * 1024 * 1024,
|
||||
retainedFileCountLimit: config.RetainedFileCountLimit,
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
|
||||
);
|
||||
|
||||
// 调试环境输出到控制台
|
||||
if (config.EnableConsole)
|
||||
loggerConfig.WriteTo.Console(
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}");
|
||||
|
||||
// 注册实时日志查看器 Sink(接收所有级别日志)| Register real-time log viewer sink (receive all levels)
|
||||
loggerConfig.WriteTo.Sink(RealTimeLogSink.Instance, Serilog.Events.LogEventLevel.Verbose);
|
||||
|
||||
// 全局初始化
|
||||
Log.Logger = loggerConfig.CreateLogger();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace XP.Common.Logging.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Serilog 自定义 Sink,将日志事件转发到 RealTimeLogViewerViewModel,并维护环形缓冲区
|
||||
/// Custom Serilog Sink that forwards log events and maintains a ring buffer for history
|
||||
/// </summary>
|
||||
public class RealTimeLogSink : ILogEventSink
|
||||
{
|
||||
private const int BufferCapacity = 500;
|
||||
private readonly ConcurrentQueue<LogEvent> _buffer = new();
|
||||
|
||||
/// <summary>
|
||||
/// 全局单例实例 | Global singleton instance
|
||||
/// </summary>
|
||||
public static RealTimeLogSink Instance { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 日志事件到达时触发 | Fired when a log event arrives
|
||||
/// </summary>
|
||||
public event Action<LogEvent>? LogEventReceived;
|
||||
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
// 入队缓冲区 | Enqueue to buffer
|
||||
_buffer.Enqueue(logEvent);
|
||||
while (_buffer.Count > BufferCapacity)
|
||||
{
|
||||
_buffer.TryDequeue(out _);
|
||||
}
|
||||
|
||||
LogEventReceived?.Invoke(logEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓冲区中的历史日志快照 | Get a snapshot of buffered history logs
|
||||
/// </summary>
|
||||
public List<LogEvent> GetBufferedHistory()
|
||||
{
|
||||
return _buffer.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using Serilog.Events;
|
||||
using XP.Common.Localization;
|
||||
|
||||
namespace XP.Common.Logging.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 实时日志查看器 ViewModel,管理日志条目、过滤、自动滚动等逻辑
|
||||
/// Real-time log viewer ViewModel, manages log entries, filtering, auto-scroll, etc.
|
||||
/// </summary>
|
||||
public class RealTimeLogViewerViewModel : BindableBase
|
||||
{
|
||||
private readonly Dispatcher _dispatcher;
|
||||
private readonly ObservableCollection<LogDisplayEntry> _allEntries = new();
|
||||
private string _filterText = string.Empty;
|
||||
private bool _isAutoScroll = true;
|
||||
private int _maxLines = 2000;
|
||||
private int _totalCount;
|
||||
private int _filteredCount;
|
||||
private bool _showDebug = true;
|
||||
private bool _showInfo = true;
|
||||
private bool _showWarning = true;
|
||||
private bool _showError = true;
|
||||
private bool _showFatal = true;
|
||||
|
||||
public RealTimeLogViewerViewModel()
|
||||
{
|
||||
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
|
||||
FilteredEntries = new ObservableCollection<LogDisplayEntry>();
|
||||
ClearCommand = new DelegateCommand(ClearAll);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤后的日志条目集合(绑定到 UI)| Filtered log entries (bound to UI)
|
||||
/// </summary>
|
||||
public ObservableCollection<LogDisplayEntry> FilteredEntries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 过滤关键词 | Filter keyword
|
||||
/// </summary>
|
||||
public string FilterText
|
||||
{
|
||||
get => _filterText;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _filterText, value))
|
||||
{
|
||||
ApplyFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否自动滚动到底部 | Whether to auto-scroll to bottom
|
||||
/// </summary>
|
||||
public bool IsAutoScroll
|
||||
{
|
||||
get => _isAutoScroll;
|
||||
set => SetProperty(ref _isAutoScroll, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最大行数限制 | Maximum line count limit
|
||||
/// </summary>
|
||||
public int MaxLines
|
||||
{
|
||||
get => _maxLines;
|
||||
set => SetProperty(ref _maxLines, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 总日志条数 | Total log entry count
|
||||
/// </summary>
|
||||
public int TotalCount
|
||||
{
|
||||
get => _totalCount;
|
||||
private set => SetProperty(ref _totalCount, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤后的日志条数 | Filtered log entry count
|
||||
/// </summary>
|
||||
public int FilteredCount
|
||||
{
|
||||
get => _filteredCount;
|
||||
private set => SetProperty(ref _filteredCount, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Debug 级别日志 | Whether to show Debug level logs
|
||||
/// </summary>
|
||||
public bool ShowDebug
|
||||
{
|
||||
get => _showDebug;
|
||||
set { if (SetProperty(ref _showDebug, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Info 级别日志 | Whether to show Information level logs
|
||||
/// </summary>
|
||||
public bool ShowInfo
|
||||
{
|
||||
get => _showInfo;
|
||||
set { if (SetProperty(ref _showInfo, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Warning 级别日志 | Whether to show Warning level logs
|
||||
/// </summary>
|
||||
public bool ShowWarning
|
||||
{
|
||||
get => _showWarning;
|
||||
set { if (SetProperty(ref _showWarning, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Error 级别日志 | Whether to show Error level logs
|
||||
/// </summary>
|
||||
public bool ShowError
|
||||
{
|
||||
get => _showError;
|
||||
set { if (SetProperty(ref _showError, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Fatal 级别日志 | Whether to show Fatal level logs
|
||||
/// </summary>
|
||||
public bool ShowFatal
|
||||
{
|
||||
get => _showFatal;
|
||||
set { if (SetProperty(ref _showFatal, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态栏文本 | Status bar text
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get
|
||||
{
|
||||
bool hasLevelFilter = !(_showDebug && _showInfo && _showWarning && _showError && _showFatal);
|
||||
bool hasTextFilter = !string.IsNullOrEmpty(_filterText);
|
||||
if (hasLevelFilter || hasTextFilter)
|
||||
return LocalizationHelper.Get("LogViewer_StatusFiltered", TotalCount, FilteredCount);
|
||||
return LocalizationHelper.Get("LogViewer_StatusTotal", TotalCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空日志命令 | Clear log command
|
||||
/// </summary>
|
||||
public DelegateCommand ClearCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动滚动请求事件,View 层订阅此事件执行滚动 | Auto-scroll request event
|
||||
/// </summary>
|
||||
public event Action? ScrollToBottomRequested;
|
||||
|
||||
/// <summary>
|
||||
/// 添加日志事件(线程安全,可从任意线程调用)| Add log event (thread-safe)
|
||||
/// </summary>
|
||||
public void AddLogEvent(LogEvent logEvent)
|
||||
{
|
||||
if (logEvent == null) return;
|
||||
|
||||
var entry = new LogDisplayEntry(logEvent);
|
||||
|
||||
if (_dispatcher.CheckAccess())
|
||||
{
|
||||
AddEntryInternal(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dispatcher.InvokeAsync(() => AddEntryInternal(entry));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部添加日志条目 | Internal add log entry
|
||||
/// </summary>
|
||||
private void AddEntryInternal(LogDisplayEntry entry)
|
||||
{
|
||||
_allEntries.Add(entry);
|
||||
|
||||
// 超出最大行数时移除最旧的条目 | Remove oldest entries when exceeding max lines
|
||||
while (_allEntries.Count > _maxLines)
|
||||
{
|
||||
var removed = _allEntries[0];
|
||||
_allEntries.RemoveAt(0);
|
||||
FilteredEntries.Remove(removed);
|
||||
}
|
||||
|
||||
// 判断是否匹配过滤条件 | Check if entry matches filter
|
||||
if (MatchesFilter(entry))
|
||||
{
|
||||
FilteredEntries.Add(entry);
|
||||
}
|
||||
|
||||
TotalCount = _allEntries.Count;
|
||||
FilteredCount = FilteredEntries.Count;
|
||||
RaisePropertyChanged(nameof(StatusText));
|
||||
|
||||
// 请求自动滚动 | Request auto-scroll
|
||||
if (_isAutoScroll)
|
||||
{
|
||||
ScrollToBottomRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用过滤器 | Apply filter
|
||||
/// </summary>
|
||||
private void ApplyFilter()
|
||||
{
|
||||
FilteredEntries.Clear();
|
||||
|
||||
foreach (var entry in _allEntries)
|
||||
{
|
||||
if (MatchesFilter(entry))
|
||||
{
|
||||
FilteredEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
FilteredCount = FilteredEntries.Count;
|
||||
RaisePropertyChanged(nameof(StatusText));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断条目是否匹配过滤条件 | Check if entry matches filter
|
||||
/// </summary>
|
||||
private bool MatchesFilter(LogDisplayEntry entry)
|
||||
{
|
||||
// 级别过滤 | Level filter
|
||||
var levelMatch = entry.LevelFull switch
|
||||
{
|
||||
"Debug" => _showDebug,
|
||||
"Information" => _showInfo,
|
||||
"Warning" => _showWarning,
|
||||
"Error" => _showError,
|
||||
"Fatal" => _showFatal,
|
||||
_ => true
|
||||
};
|
||||
if (!levelMatch) return false;
|
||||
|
||||
// 关键词过滤 | Keyword filter
|
||||
if (string.IsNullOrWhiteSpace(_filterText)) return true;
|
||||
return entry.Message.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|
||||
|| entry.Level.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|
||||
|| entry.Source.Contains(_filterText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有日志 | Clear all logs
|
||||
/// </summary>
|
||||
private void ClearAll()
|
||||
{
|
||||
_allEntries.Clear();
|
||||
FilteredEntries.Clear();
|
||||
TotalCount = 0;
|
||||
FilteredCount = 0;
|
||||
RaisePropertyChanged(nameof(StatusText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志显示条目模型 | Log display entry model
|
||||
/// </summary>
|
||||
public class LogDisplayEntry
|
||||
{
|
||||
public LogDisplayEntry(LogEvent logEvent)
|
||||
{
|
||||
Timestamp = logEvent.Timestamp.LocalDateTime;
|
||||
TimestampDisplay = Timestamp.ToString("HH:mm:ss.fff");
|
||||
Level = logEvent.Level.ToString().Substring(0, 3).ToUpper();
|
||||
LevelFull = logEvent.Level.ToString();
|
||||
|
||||
// 渲染消息文本 | Render message text
|
||||
Message = logEvent.RenderMessage();
|
||||
|
||||
// 如果有异常,追加异常信息 | Append exception info if present
|
||||
if (logEvent.Exception != null)
|
||||
{
|
||||
Message += Environment.NewLine + logEvent.Exception.ToString();
|
||||
}
|
||||
|
||||
// 提取 SourceContext | Extract SourceContext
|
||||
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceValue))
|
||||
{
|
||||
Source = sourceValue.ToString().Trim('"');
|
||||
}
|
||||
else
|
||||
{
|
||||
Source = string.Empty;
|
||||
}
|
||||
|
||||
// 根据日志级别设置颜色 | Set color based on log level
|
||||
LevelColor = logEvent.Level switch
|
||||
{
|
||||
LogEventLevel.Fatal => new SolidColorBrush(Color.FromRgb(180, 0, 0)),
|
||||
LogEventLevel.Error => new SolidColorBrush(Colors.Red),
|
||||
LogEventLevel.Warning => new SolidColorBrush(Color.FromRgb(200, 130, 0)),
|
||||
LogEventLevel.Debug => new SolidColorBrush(Colors.Gray),
|
||||
LogEventLevel.Verbose => new SolidColorBrush(Colors.LightGray),
|
||||
_ => new SolidColorBrush(Color.FromRgb(30, 30, 30))
|
||||
};
|
||||
|
||||
// Freeze 画刷,使其可跨线程访问(避免 DependencySource 线程异常)
|
||||
// Freeze the brush so it can be accessed from any thread
|
||||
LevelColor.Freeze();
|
||||
}
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
public string TimestampDisplay { get; }
|
||||
public string Level { get; }
|
||||
public string LevelFull { get; }
|
||||
public string Message { get; }
|
||||
public string Source { get; }
|
||||
public SolidColorBrush LevelColor { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<Window x:Class="XP.Common.GeneralForm.Views.RealTimeLogViewer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
|
||||
Title="{loc:Localization LogViewer_Title}"
|
||||
Width="960" Height="600"
|
||||
MinWidth="700" MinHeight="400"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="True"
|
||||
WindowStyle="SingleBorderWindow">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 顶部工具栏 | Top toolbar -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 级别筛选栏 | Level filter bar -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 日志显示区 | Log display area -->
|
||||
<RowDefinition Height="*"/>
|
||||
<!-- 底部状态栏 | Bottom status bar -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- === 顶部工具栏 | Top Toolbar === -->
|
||||
<DockPanel Grid.Row="0" Margin="8,6" LastChildFill="True">
|
||||
<!-- 左侧:自动滚动开关 + 清空按钮 | Left: Auto-scroll toggle + Clear button -->
|
||||
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<telerik:RadToggleButton x:Name="AutoScrollToggle"
|
||||
telerik:StyleManager.Theme="Crystal"
|
||||
IsChecked="{Binding IsAutoScroll, Mode=TwoWay}"
|
||||
Content="{loc:Localization LogViewer_AutoScroll}"
|
||||
Padding="10,4" Margin="0,0,6,0"/>
|
||||
<telerik:RadButton telerik:StyleManager.Theme="Crystal"
|
||||
Command="{Binding ClearCommand}"
|
||||
Content="{loc:Localization LogViewer_ClearLog}"
|
||||
Padding="10,4" Margin="0,0,12,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧:过滤输入框 | Right: Filter input -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock Text="{loc:Localization LogViewer_Filter}" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<telerik:RadWatermarkTextBox telerik:StyleManager.Theme="Crystal"
|
||||
Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged, Delay=300}"
|
||||
WatermarkContent="{loc:Localization LogViewer_FilterWatermark}"
|
||||
Width="260" Padding="4,3"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<!-- === 级别筛选栏 | Level Filter Bar === -->
|
||||
<Border Grid.Row="1" Background="#FFF8F8F8" BorderBrush="#FFDDDDDD" BorderThickness="0,0,0,1" Padding="8,4">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="{loc:Localization LogViewer_LevelFilter}" VerticalAlignment="Center" Margin="0,0,8,0" Foreground="#FF666666" FontSize="12"/>
|
||||
<CheckBox Content="Debug" IsChecked="{Binding ShowDebug}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Gray"/>
|
||||
<CheckBox Content="Info" IsChecked="{Binding ShowInfo}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FF1E1E1E"/>
|
||||
<CheckBox Content="Warning" IsChecked="{Binding ShowWarning}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFC88200"/>
|
||||
<CheckBox Content="Error" IsChecked="{Binding ShowError}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Red"/>
|
||||
<CheckBox Content="Fatal" IsChecked="{Binding ShowFatal}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFB40000"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- === 日志显示区(RadGridView)| Log Display Area === -->
|
||||
<telerik:RadGridView Grid.Row="2"
|
||||
x:Name="LogGridView"
|
||||
telerik:StyleManager.Theme="Crystal"
|
||||
ItemsSource="{Binding FilteredEntries}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
RowIndicatorVisibility="Collapsed"
|
||||
ShowGroupPanel="False"
|
||||
ShowColumnHeaders="True"
|
||||
CanUserFreezeColumns="False"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserSortColumns="False"
|
||||
CanUserResizeColumns="True"
|
||||
IsFilteringAllowed="False"
|
||||
SelectionMode="Extended"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12"
|
||||
Margin="4,0,4,0">
|
||||
<telerik:RadGridView.Columns>
|
||||
<!-- 时间戳列 | Timestamp column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColTime}"
|
||||
DataMemberBinding="{Binding TimestampDisplay}"
|
||||
Width="100"
|
||||
IsReadOnly="True"/>
|
||||
|
||||
<!-- 级别列 | Level column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColLevel}"
|
||||
DataMemberBinding="{Binding Level}"
|
||||
Width="60"
|
||||
IsReadOnly="True">
|
||||
<telerik:GridViewDataColumn.CellStyle>
|
||||
<Style TargetType="telerik:GridViewCell">
|
||||
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
</telerik:GridViewDataColumn.CellStyle>
|
||||
</telerik:GridViewDataColumn>
|
||||
|
||||
<!-- 来源列 | Source column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColSource}"
|
||||
DataMemberBinding="{Binding Source}"
|
||||
Width="200"
|
||||
IsReadOnly="True">
|
||||
<telerik:GridViewDataColumn.CellStyle>
|
||||
<Style TargetType="telerik:GridViewCell">
|
||||
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||
</Style>
|
||||
</telerik:GridViewDataColumn.CellStyle>
|
||||
</telerik:GridViewDataColumn>
|
||||
|
||||
<!-- 消息列 | Message column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColMessage}"
|
||||
DataMemberBinding="{Binding Message}"
|
||||
Width="*"
|
||||
IsReadOnly="True">
|
||||
<telerik:GridViewDataColumn.CellStyle>
|
||||
<Style TargetType="telerik:GridViewCell">
|
||||
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||
</Style>
|
||||
</telerik:GridViewDataColumn.CellStyle>
|
||||
</telerik:GridViewDataColumn>
|
||||
</telerik:RadGridView.Columns>
|
||||
</telerik:RadGridView>
|
||||
|
||||
<!-- === 底部状态栏 | Bottom Status Bar === -->
|
||||
<StatusBar Grid.Row="3" Background="#FFF0F0F0" BorderBrush="#FFCCCCCC" BorderThickness="0,1,0,0">
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="{Binding StatusText}" Foreground="#FF666666" FontSize="12"/>
|
||||
</StatusBarItem>
|
||||
<Separator/>
|
||||
<StatusBarItem HorizontalAlignment="Right">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{loc:Localization LogViewer_MaxLines}" Foreground="#FF999999" FontSize="11" VerticalAlignment="Center"/>
|
||||
<telerik:RadNumericUpDown telerik:StyleManager.Theme="Crystal"
|
||||
Value="{Binding MaxLines, Mode=TwoWay}"
|
||||
Minimum="100" Maximum="10000"
|
||||
NumberDecimalDigits="0"
|
||||
SmallChange="500"
|
||||
Width="90" Height="22"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using Serilog.Events;
|
||||
using XP.Common.Logging.ViewModels;
|
||||
|
||||
namespace XP.Common.GeneralForm.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 实时日志查看器窗口,订阅 Serilog 事件并实时显示
|
||||
/// Real-time log viewer window, subscribes to Serilog events and displays in real-time
|
||||
/// </summary>
|
||||
public partial class RealTimeLogViewer : Window
|
||||
{
|
||||
private readonly RealTimeLogViewerViewModel _viewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="maxLines">最大行数限制,默认 2000 | Max line count, default 2000</param>
|
||||
public RealTimeLogViewer(int maxLines = 2000)
|
||||
{
|
||||
_viewModel = new RealTimeLogViewerViewModel { MaxLines = maxLines };
|
||||
DataContext = _viewModel;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// 继承主窗口图标 | Inherit main window icon
|
||||
if (Application.Current?.MainWindow != null)
|
||||
{
|
||||
Icon = Application.Current.MainWindow.Icon;
|
||||
}
|
||||
|
||||
// 订阅自动滚动事件 | Subscribe to auto-scroll event
|
||||
_viewModel.ScrollToBottomRequested += OnScrollToBottomRequested;
|
||||
|
||||
// 加载缓冲区中的历史日志 | Load buffered history logs
|
||||
var history = RealTimeLogSink.Instance.GetBufferedHistory();
|
||||
foreach (var logEvent in history)
|
||||
{
|
||||
_viewModel.AddLogEvent(logEvent);
|
||||
}
|
||||
|
||||
// 订阅 Serilog Sink 事件(在历史加载之后,避免重复)| Subscribe after history load
|
||||
RealTimeLogSink.Instance.LogEventReceived += OnLogEventReceived;
|
||||
|
||||
Closed += OnWindowClosed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接收 Serilog 日志事件 | Receive Serilog log event
|
||||
/// </summary>
|
||||
private void OnLogEventReceived(LogEvent logEvent)
|
||||
{
|
||||
_viewModel.AddLogEvent(logEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动滚动到底部 | Auto-scroll to bottom
|
||||
/// </summary>
|
||||
private void OnScrollToBottomRequested()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (LogGridView.Items.Count > 0)
|
||||
{
|
||||
var lastItem = LogGridView.Items[LogGridView.Items.Count - 1];
|
||||
LogGridView.ScrollIntoView(lastItem);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 滚动失败时静默处理 | Silently handle scroll failures
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口关闭时取消订阅,防止内存泄漏 | Unsubscribe on close to prevent memory leaks
|
||||
/// </summary>
|
||||
private void OnWindowClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_viewModel.ScrollToBottomRequested -= OnScrollToBottomRequested;
|
||||
RealTimeLogSink.Instance.LogEventReceived -= OnLogEventReceived;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user