将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user