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 { /// /// 实时日志查看器 ViewModel,管理日志条目、过滤、自动滚动等逻辑 /// Real-time log viewer ViewModel, manages log entries, filtering, auto-scroll, etc. /// public class RealTimeLogViewerViewModel : BindableBase { private readonly Dispatcher _dispatcher; private readonly ObservableCollection _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(); ClearCommand = new DelegateCommand(ClearAll); } /// /// 过滤后的日志条目集合(绑定到 UI)| Filtered log entries (bound to UI) /// public ObservableCollection FilteredEntries { get; } /// /// 过滤关键词 | Filter keyword /// public string FilterText { get => _filterText; set { if (SetProperty(ref _filterText, value)) { ApplyFilter(); } } } /// /// 是否自动滚动到底部 | Whether to auto-scroll to bottom /// public bool IsAutoScroll { get => _isAutoScroll; set => SetProperty(ref _isAutoScroll, value); } /// /// 最大行数限制 | Maximum line count limit /// public int MaxLines { get => _maxLines; set => SetProperty(ref _maxLines, value); } /// /// 总日志条数 | Total log entry count /// public int TotalCount { get => _totalCount; private set => SetProperty(ref _totalCount, value); } /// /// 过滤后的日志条数 | Filtered log entry count /// public int FilteredCount { get => _filteredCount; private set => SetProperty(ref _filteredCount, value); } /// /// 是否显示 Debug 级别日志 | Whether to show Debug level logs /// public bool ShowDebug { get => _showDebug; set { if (SetProperty(ref _showDebug, value)) ApplyFilter(); } } /// /// 是否显示 Info 级别日志 | Whether to show Information level logs /// public bool ShowInfo { get => _showInfo; set { if (SetProperty(ref _showInfo, value)) ApplyFilter(); } } /// /// 是否显示 Warning 级别日志 | Whether to show Warning level logs /// public bool ShowWarning { get => _showWarning; set { if (SetProperty(ref _showWarning, value)) ApplyFilter(); } } /// /// 是否显示 Error 级别日志 | Whether to show Error level logs /// public bool ShowError { get => _showError; set { if (SetProperty(ref _showError, value)) ApplyFilter(); } } /// /// 是否显示 Fatal 级别日志 | Whether to show Fatal level logs /// public bool ShowFatal { get => _showFatal; set { if (SetProperty(ref _showFatal, value)) ApplyFilter(); } } /// /// 状态栏文本 | Status bar text /// 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); } } /// /// 清空日志命令 | Clear log command /// public DelegateCommand ClearCommand { get; } /// /// 自动滚动请求事件,View 层订阅此事件执行滚动 | Auto-scroll request event /// public event Action? ScrollToBottomRequested; /// /// 添加日志事件(线程安全,可从任意线程调用)| Add log event (thread-safe) /// public void AddLogEvent(LogEvent logEvent) { if (logEvent == null) return; var entry = new LogDisplayEntry(logEvent); if (_dispatcher.CheckAccess()) { AddEntryInternal(entry); } else { _dispatcher.InvokeAsync(() => AddEntryInternal(entry)); } } /// /// 内部添加日志条目 | Internal add log entry /// 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(); } } /// /// 应用过滤器 | Apply filter /// private void ApplyFilter() { FilteredEntries.Clear(); foreach (var entry in _allEntries) { if (MatchesFilter(entry)) { FilteredEntries.Add(entry); } } FilteredCount = FilteredEntries.Count; RaisePropertyChanged(nameof(StatusText)); } /// /// 判断条目是否匹配过滤条件 | Check if entry matches filter /// 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); } /// /// 清空所有日志 | Clear all logs /// private void ClearAll() { _allEntries.Clear(); FilteredEntries.Clear(); TotalCount = 0; FilteredCount = 0; RaisePropertyChanged(nameof(StatusText)); } } /// /// 日志显示条目模型 | Log display entry model /// 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; } } }