330 lines
11 KiB
C#
330 lines
11 KiB
C#
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; }
|
||
}
|
||
}
|