从零搭建灌装监控系统(十五):日志查看器,筛选与分页

发布时间:2026/10/8 8:18:18

从零搭建灌装监控系统(十五):日志查看器,筛选与分页 日志查看器筛选与分页这是「从零搭建灌装监控系统」系列第15篇。上一篇把日志写进了 SQLite但能保存不等于能使用。日志查看器要让操作员快速找到一段时间内的错误也要让开发人员按级别和关键字缩小范围。这篇围绕LogsViewModel实现查询条件、50条分页、忙碌状态和导出入口。日志很多真正有用的只有几行设备断线那天日志窗口里有几千条轮询信息。操作员滚动了很久还是没找到第一次断线的时间开发人员把数据库复制回来后发现日志表里有数据但没有日期过滤和级别筛选。“把日志全部显示出来”不是日志查看器。一个好用的查看器要让人先缩小范围再看细节哪一天、什么级别、包含哪个关键词。默认只加载最近一页避免启动时把整个日志表拖进内存。ViewModel 先确定状态publicpartialclassLogsViewModel:ObservableObject{privateconstintPageSize50;[ObservableProperty]privateObservableCollectionSystemLog_logsnew();[ObservableProperty]privateDateTime_startDateDateTime.Today;[ObservableProperty]privateDateTime_endDateDateTime.Today;[ObservableProperty]privatestring_selectedLevelAll;[ObservableProperty]privatestring_keyword;[ObservableProperty]privateint_pageIndex1;[ObservableProperty]privateint_totalCount;[ObservableProperty]privatebool_isBusy;publicintPageCountMath.Max(1,(int)Math.Ceiling((double)TotalCount/PageSize));}页面需要的状态都集中在 ViewModel筛选条件、当前页、总数、日志集合和忙碌状态。这样 XAML 只负责绑定不需要知道查询到底用的是 FreeSql 还是其他实现。日期结束值默认是今天但查询时不能直接把今天 00:00 当成结束时间否则当天日志会全部漏掉。统一把结束日期加一天使用[start, end)。BuildQuery条件按需拼接privateISelectSystemLogBuildQuery(){varendEndDate.Date.AddDays(1);varqueryDbProvider.Fsql.SelectSystemLog().Where(xx.TimestampStartDate.Date).Where(xx.Timestampend);if(!string.IsNullOrWhiteSpace(SelectedLevel)SelectedLevel!All){queryquery.Where(xx.LevelSelectedLevel);}if(!string.IsNullOrWhiteSpace(Keyword)){varkeywordKeyword.Trim();queryquery.Where(xx.Message.Contains(keyword)||x.Exception.Contains(keyword));}returnquery;}查询条件要在数据库端过滤不要先ToListAsync()再用 LINQ to Objects 筛选。后者会把无关日志全部加载到内存数据越多越慢。Exception允许为空时要确认 ORM 对空值和Contains的翻译行为。不同数据库的字符串查询细节不完全一致遇到异常时打开 SQL 监控比盯着 UI 猜更快。关键字搜索也不应该默认做复杂的全文检索。单机日志几万条时普通Contains够用数据量上来后再考虑索引或专门的全文检索方案。加载一页而不是加载全部privateasyncTaskLoadLogsAsync(){if(IsBusy){LogService.Warn(日志查询被跳过当前仍在加载);return;}IsBusytrue;try{varqueryBuildQuery();TotalCount(int)awaitquery.CountAsync();vardataawaitquery.OrderByDescending(xx.Timestamp).Page(PageIndex,PageSize).ToListAsync();LogsnewObservableCollectionSystemLog(data);OnPropertyChanged(nameof(PageCount));}catch(Exceptionex){LogService.Error(加载日志失败,ex);Logs.Clear();TotalCount0;}finally{IsBusyfalse;}}这里有两个查询一个拿总数一个拿当前页。它们必须复用同一个BuildQuery否则用户看到的页数和内容会不一致。页面固定 50 条是性能和可读性的折中。太小会让用户频繁翻页太大则让 UI 列表和渲染压力增加。这个数字应该根据实际屏幕尺寸和日志字段长度调整不是一个神奇常数。修改筛选条件PageIndex 重置为 1BuildQuery 拼装条件CountAsync 取总数排序后 Page 取当前页替换 ObservableCollection刷新 PageCount搜索时重置页码用户在第 8 页输入关键字新结果可能只有 1 页。如果不重置页码查询会直接返回空列表用户很容易以为没有匹配内容。[RelayCommand]privateasyncTaskSearchAsync(){PageIndex1;awaitLoadLogsAsync();}清空筛选也应该走同一条路径。不要在 UI 里复制一套“清空集合、重新加载、更新总数”的逻辑所有筛选变化最终都调用查询入口。翻页命令要限制边界[RelayCommand]privateasyncTaskPreviousPageAsync(){if(PageIndex1||IsBusy)return;PageIndex--;awaitLoadLogsAsync();}[RelayCommand]privateasyncTaskNextPageAsync(){if(PageIndexPageCount||IsBusy)return;PageIndex;awaitLoadLogsAsync();}按钮可用状态可以绑定PageIndex和PageCount但命令内部仍然要判断。ViewModel 可能被测试直接调用不能把正确性完全交给界面。如果日志在翻页期间持续写入总数可能发生变化。普通查看器不必追求实时一致可以在每次查询时重新统计如果用户正在查看历史日志固定end时间会让页面更稳定。搜索防抖和取消关键字输入框如果每敲一个字就查数据库用户输入一串词会触发多次查询。简单做法是只在点击搜索按钮时执行如果产品要求实时搜索可以加 300ms 防抖和取消令牌privateCancellationTokenSource?_searchCts;privateasyncTaskSearchWithDebounceAsync(){_searchCts?.Cancel();_searchCts?.Dispose();_searchCtsnewCancellationTokenSource();try{awaitTask.Delay(300,_searchCts.Token);PageIndex1;awaitLoadLogsAsync(_searchCts.Token);}catch(OperationCanceledException){// 新的输入会接管查询}}取消旧查询很重要。旧查询晚一点返回时不能把结果覆盖掉用户最新的筛选条件。实际项目要让数据库调用支持 token如果 ORM 版本不支持完全取消也至少要在结果回写前检查查询版本号。时间、级别和关键字的组合用户可能只选级别也可能只输入关键字筛选器必须支持任意组合。组合查询应当在数据库侧逐步添加条件varqueryDbProvider.Fsql.SelectSystemLog();if(start.HasValue)queryquery.Where(xx.Timestampstart.Value);if(end.HasValue)queryquery.Where(xx.Timestampend.Value);if(level!All)queryquery.Where(xx.Levellevel);if(!string.IsNullOrWhiteSpace(keyword))queryquery.Where(xx.Message.Contains(keyword));不要为了“通用”把所有条件拼成一条很长的字符串。可读的条件链更容易调试也方便以后增加设备编号、日志来源或线程标识。日志级别下拉框的值要和数据库写入的级别格式统一。一个地方写Warning另一个地方写中文“警告”筛选自然就失效了。显示文本可以本地化查询值保持稳定的英文枚举名。导出当前筛选结果日志查看器通常需要一个“导出当前结果”按钮。不要重新实现 CSV 逻辑而是把当前查询条件传给导出服务[RelayCommand]privateasyncTaskExportAsync(){varpathawaitSelectExportPathAsync();if(pathnull)return;IsBusytrue;try{varresultawaitLogExportService.ExportAsync(BuildQuery(),path);StatusText$已导出{result}条日志;}catch(Exceptionex){LogService.Error(导出日志失败,ex);StatusText导出失败请检查文件路径;}finally{IsBusyfalse;}}如果导出的是当前筛选结果导出服务最好接收查询条件或导出模型而不是接收页面上的Logs集合。页面集合只有当前 50 条直接导出会让用户误以为导出了全部结果。XAML 显示层的几个小细节日志列表常见字段是时间、级别和消息。异常堆栈可以放到详情面板不要让每行都展开很长的文本DataGridItemsSource{Binding Logs}IsReadOnlyTrueAutoGenerateColumnsFalseDataGrid.ColumnsDataGridTextColumnHeader时间Binding{Binding Timestamp}/DataGridTextColumnHeader级别Binding{Binding Level}/DataGridTextColumnHeader消息Binding{Binding Message}//DataGrid.Columns/DataGrid级别颜色可以通过 DataTrigger 实现但颜色只是提示不要只用红绿区分。文字级别和筛选条件必须始终可见避免色觉差异或低亮度环境下看不清。踩坑记录搜索不重置页码第 10 页过滤后只剩 1 页结果为空。搜索命令必须把页码重置为 1。把当前Logs集合导出集合只有当前页导出结果少得离谱。导出应复用完整查询条件。直接在 UI 线程查询数据库数据量一大窗口就卡住。命令方法使用异步查询UI 只在结果返回后更新集合。条件和总数使用两套代码总数显示 200 条翻页却只有 3 页通常就是过滤条件没有复用。页面销毁后仍收到日志事件订阅不解除会导致重复刷新甚至内存泄漏。页面关闭或 ViewModel 销毁时解除事件订阅。本篇小结功能实现要点默认加载最近时间范围 第一页分页固定 50 条数据库端 Page搜索条件链 重置页码防重复查询IsBusy、防抖或取消令牌导出复用完整筛选条件UI 安全后台查询Dispatcher 更新集合第三批和第四批到这里就连起来了设备数据进入数据库配置安全保存报警和日志可追溯操作员还能按条件查看。后面的 Dashboard、实时曲线和自定义控件会把这些数据重新带回操作界面。下期预告第16篇Dashboard 设计KPI卡片与液位可视化下一篇开始 UI 与可视化批次把数据库和通信状态整理成操作员一眼能看懂的首页指标。
延伸阅读

更多相关文章

2026/10/8 8:18:18

Blue---贪心

前言 贪心算法的学习重点我想放在见多识广上,轻证明甚至不证明,如果一道题你没有任何思路,不妨想想贪心能不能做,当然,其实我们能想到的贪心策略取决于我们做过多少道题。一般就是题目里说求什么最少,什么最…

2026/10/8 9:03:30

保姆级教程:Windows下MySQL 9.1.0安装全流程解析

MySQL 9.1.0 发布之后,这段时间经常有人来问我同一个问题:不是问它跟 8.4 LTS 到底差多少,而是问“怎么装”。也确实,MySQL 官网的下载页对新手来说就是一本天书,一堆版本号横七竖八地排在那里,下面还有 ZI…

2026/10/8 9:03:30

HDMI2.1与eDP TX接口设计实战:从眼图测试到信号完整性排查

一块板子拿到手,第一次插上显示器就花屏或者直接黑屏,这种场景做硬件的人应该都不陌生。HDMI2.1、eDP这类高速视频TX接口,说难其实不算难,但坑的位置非常固定:高速差分信号怎么走、AC耦合电容放哪边、阻抗控制到多少、…

2026/10/8 9:03:30

双碳大模型实战:碳核算报告生成与CCUS比选

简介:一份聚焦大模型技术在碳排放与碳回收(双碳)领域应用的系统方案,内容从全球碳排放现状背景讲起,梳理工业化、能源消耗、交通、农业等主要驱动因素,并详细介绍化学吸收法、膜分离法、生物固定法、物理吸…

2026/10/8 9:03:30

OpenClaw(龙虾)部署实战:从Windows、安卓到腾讯云免费算力

说实话,我一开始看到“龙虾”OpenClaw全国巡装、腾讯云免费装机这种消息,第一反应是:这又是什么圈子里的新梗?结果顺着关键词一查,才发现这压根不是玩梗,而是一个正在快速升温的AI个人助理开发项目在往线下…

2026/10/8 9:03:30

Canal启动报错:Could not find first log file name 根因排查与解决

最近在帮团队搭建数据同步管道,启动Canal时报了一个看起来挺唬人的错误:Could not find first log file name in binary log index file。这个错误估计不少用过Canal的朋友都撞上过,第一次看到的时候我还愣了一下,毕竟Canal已经配…

2026/10/8 8:58:29

蠕虫病毒传播链与分层防御:从应急响应到内网加固实战指南

周五晚上十点,我正在家看球赛,手机突然连震三次。值班同事在群里发消息:核心交换机流量异常,内网大量主机互相发包,OA系统已经打不开了。紧接着远程连服务器,ssh敲下去卡了十几秒才出提示符,upt…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/7 8:18:33

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 0:02:17

自然数立方等于连续奇数之和:从证明到编程验证

十几年来我一直游走在数学科普和编程教学这两块内容之间,对“看起来像魔法、拆开全是数学”的结论总是格外敏感。最近翻资料时又撞见一句话:任何一个自然数 m 的立方,都可以写成 m 个连续奇数之和。2 的立方等于 3 加 5,3 的立方等…

2026/10/8 0:02:17

C#上位机SSH连接实战:用SSH.NET补齐超时、批量与密钥认证

简介:这是一份基于 C# 开发的 SSH 连接功能半成品工程,原本作为另一个主项目的子功能模块,现独立打包分享。工程采用 WinForms 界面,包含源码、解决方案、安装部署工程、NuGet 依赖包及说明文档,适合正在做远程连接、网…

2026/10/8 0:02:17

Java SpringBoot一体化智能售后系统设计与实现全解析

毕业设计年年做,Java Web 方向的题目翻来覆去就那么几个,但“一体化智能售后系统”这个题,每次看到我都觉得值得认真聊一聊。它不是一个简单 curd 堆出来的管理系统,而是把客户、工单、派单、处理、回访、统计整条链路串起来的一套…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑