跳转至

日志查看器 LogViewer API

src.components.LogViewer.log_viewer_demo.LogCheckForm

Bases: QWidget, Ui_log_viewer

Source code in src/components/LogViewer/log_viewer_demo.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
class LogCheckForm(QWidget, Ui_log_viewer):
    DATE_FORMAT = '%Y-%m-%d'

    def __init__(self):
        super(LogCheckForm, self).__init__()
        self.setupUi(self)
        self.InitUI()

    def InitUI(self):
        self.setWindowTitle('历史日志')
        strdate = datetime.now().strftime(self.DATE_FORMAT)
        self.current_log_content = ""  # 存储当前完整日志内容
        self.batch_mode = False  # 批量删除模式标志

        # 使用统一的日志显示工具类,无需重复定义正则表达式

        # 设置列表控件支持多选
        self.listWidget_historyLogs.setSelectionMode(QAbstractItemView.ExtendedSelection)

        # 拖拽勾选:初始化并安装事件过滤器(仅在批量模式生效)
        self._drag_check_active = False
        self._drag_check_target_state = None
        self._drag_checked_indexes = set()
        self._drag_autoscroll_margin = 24  # 视口上下边缘触发自动滚动的边距(像素)
        self.listWidget_historyLogs.setMouseTracking(True)
        self.listWidget_historyLogs.viewport().installEventFilter(self)

        # 设置右键菜单
        self.listWidget_historyLogs.setContextMenuPolicy(Qt.CustomContextMenu)
        self.listWidget_historyLogs.customContextMenuRequested.connect(self.Show_Context_Menu)
        self.listWidget_historyLogs.itemClicked.connect(self.On_History_Log_Clicked)

        # 初始化加载与信号连接(之前误删,现恢复)
        self.Get_Log_File_By_Date(strdate)
        self.Set_Log_Date()
        self.calendarWidget.selectionChanged.connect(
            lambda: self.Get_Log_File_By_Date(self.calendarWidget.selectedDate().toString("yyyy-MM-dd"))
        )
        self.comboBox_logType.currentTextChanged.connect(self.Filter_Log_By_Type)

        # 连接按钮事件
        self.pushButton_batchMode.clicked.connect(self.Enter_Batch_Mode)
        self.pushButton_deleteSelected.clicked.connect(self.Delete_Selected_Logs)
        self.pushButton_cancelBatch.clicked.connect(self.Exit_Batch_Mode)
        self.pushButton_deleteByDate.clicked.connect(self.Delete_Logs_By_Date)

        # 设置按钮样式
        self.Set_Button_Styles()

        self.Load_History_Log_List()
        # 设置应用图标
        self._set_window_icon()

    def _set_window_icon(self):
        """设置窗口图标"""
        try:
            icon_path = str(Path(__file__).parent.parent.parent / "assets" / "icon" / "文件文档.svg")
            if os.path.exists(icon_path):
                self.setWindowIcon(QIcon(icon_path))
        except Exception as e:
            print(f"设置图标失败: {e}")

    def Set_Log_Date(self):
        log_files = glob.glob(os.path.join(LOG_FILES, "*.log"))
        for logfile in log_files:
            try:
                date_str = os.path.basename(logfile)
                date_parts = date_str.split('_')
                if len(date_parts) < 2:
                    continue  # 跳过不符合格式的文件名
                date_str = date_parts[1].split('.')[0]
                date = datetime.strptime(date_str, self.DATE_FORMAT).date()
                date_format_obj = QTextCharFormat()
                date_format_obj.setBackground(QColor(180, 238, 180))
                self.calendarWidget.setDateTextFormat(date, date_format_obj)
            except (IndexError, ValueError, AttributeError):
                # 忽略无法解析的文件名或日期格式错误
                continue

    def Get_Log_File_By_Date(self,date):
        log_files=glob.glob(os.path.join(LOG_FILES,"*.log"))
        for logfile in log_files:
            if date in logfile:
                with open(logfile,'r',encoding='utf-8') as file:
                    self.current_log_content=file.read()
                    self.Update_Log_Types()  # 动态更新日志类型
                    self.Filter_Log_By_Type(self.comboBox_logType.currentText())
                return
        # 如果没有找到对应日期的日志文件,清空显示
        self.current_log_content = ""
        self.plainTextEdit_log.clear()
        self.Update_Log_Types()  # 清空日志类型

    def Update_Log_Types(self):
        """动态扫描当前日志内容中的所有日志类型并更新下拉框"""
        # 保存当前选择的类型
        current_selection = self.comboBox_logType.currentText()

        # 清空下拉框
        self.comboBox_logType.clear()

        # 添加"全部"选项
        self.comboBox_logType.addItem("全部")

        if self.current_log_content:
            # 使用统一工具类提取日志级别
            log_levels = log_display_util.get_log_levels_from_content(self.current_log_content)

            # 按字母顺序排序并添加到下拉框
            for level in sorted(log_levels):
                self.comboBox_logType.addItem(level)

        # 尝试恢复之前的选择,如果不存在则选择"全部"
        index = self.comboBox_logType.findText(current_selection)
        if index >= 0:
            self.comboBox_logType.setCurrentIndex(index)
        else:
            self.comboBox_logType.setCurrentIndex(0)  # 选择"全部"

    def Filter_Log_By_Type(self, log_type):
        """根据日志类型过滤日志内容并应用颜色"""
        if not self.current_log_content:
            return

        self.plainTextEdit_log.clear()

        if log_type == "全部":
            # 显示所有日志并应用颜色
            self.Apply_Colors_To_Content(self.current_log_content)
        else:
            # 使用统一工具类按类型过滤日志并应用颜色
            filtered_content = log_display_util.filter_logs_by_level(self.current_log_content, log_type)
            self.Apply_Colors_To_Content(filtered_content)

    def Apply_Colors_To_Content(self, content):
        """应用颜色到日志内容 - 使用统一工具类"""
        # 使用统一的日志显示工具类进行颜色渲染
        log_display_util.apply_colors_to_text_widget(self.plainTextEdit_log, content)

    def Apply_Colors_Simple(self, content):
        """简化的颜色渲染模式,用于大量日志 - 使用统一工具类"""
        # 使用统一的日志显示工具类的简化模式
        log_display_util._apply_colors_simple(self.plainTextEdit_log, content)

    def Load_History_Log_List(self):
        """加载历史日志文件列表 - 优化版本"""
        try:
            self.listWidget_historyLogs.clear()
            log_files = glob.glob(os.path.join(LOG_FILES, "*.log"))
            log_files.sort()  # 按文件名排序

            # 先快速加载文件名,后续异步加载详细信息
            for logfile in log_files:
                try:
                    # 提取日期信息
                    date = os.path.basename(logfile).split('_')[1].split('.')[0]

                    # 获取文件大小作为快速指标
                    file_size = os.path.getsize(logfile)
                    size_kb = file_size // 1024

                    # 创建显示文本(先显示文件大小,避免读取文件内容)
                    display_text = f"{date} ({size_kb}KB)"

                    # 添加到列表控件
                    item = QListWidgetItem(display_text)
                    item.setData(Qt.UserRole, date)  # 存储日期信息
                    item.setData(Qt.UserRole + 1, logfile)  # 存储文件路径

                    # 根据批量模式设置复选框
                    if self.batch_mode:
                        item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                        item.setCheckState(Qt.Unchecked)
                    else:
                        item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable)

                    self.listWidget_historyLogs.addItem(item)

                except Exception as e:
                    print(f"处理文件 {logfile} 时出错: {e}")

            # 异步更新详细信息(行数统计)
            QtCore.QTimer.singleShot(100, self.Update_Log_Counts_Async)

        except Exception as e:
            print(f"加载历史日志列表时出错: {e}")

    def Update_Log_Counts_Async(self):
        """异步更新日志行数统计"""
        try:
            for i in range(self.listWidget_historyLogs.count()):
                item = self.listWidget_historyLogs.item(i)
                if item:
                    logfile = item.data(Qt.UserRole + 1)
                    date = item.data(Qt.UserRole)

                    try:
                        # 使用更高效的行数统计方法
                        log_count = self.Count_Log_Lines_Fast(logfile)

                        # 更新显示文本
                        display_text = f"{date} ({log_count}条日志)"
                        item.setText(display_text)

                        # 强制刷新UI
                        QtWidgets.QApplication.processEvents()

                    except Exception as e:
                        print(f"更新文件 {logfile} 行数时出错: {e}")

        except Exception as e:
            print(f"异步更新日志行数时出错: {e}")

    def Count_Log_Lines_Fast(self, filepath):
        """快速统计日志文件行数"""
        try:
            count = 0
            with open(filepath, 'r', encoding='utf-8') as file:
                # 使用缓冲读取,避免一次性加载整个文件
                buffer_size = 8192
                while True:
                    buffer = file.read(buffer_size)
                    if not buffer:
                        break
                    count += buffer.count('\n')
            return count
        except Exception as e:
            print(f"统计文件 {filepath} 行数时出错: {e}")
            return 0

    def On_History_Log_Clicked(self, item):
        """处理历史日志列表点击事件"""
        try:
            # 获取存储的日期信息
            date = item.data(Qt.UserRole)
            if date:
                # 临时断开日历信号连接,避免循环触发
                self.calendarWidget.selectionChanged.disconnect()

                # 设置日历控件到对应日期
                selected_date = QDate.fromString(date, "yyyy-MM-dd")
                if selected_date.isValid():
                    self.calendarWidget.setSelectedDate(selected_date)

                # 重新连接信号
                self.calendarWidget.selectionChanged.connect(lambda:self.Get_Log_File_By_Date(self.calendarWidget.selectedDate().toString("yyyy-MM-dd")))

                # 加载对应日期的日志
                self.Get_Log_File_By_Date(date)
        except Exception as e:
            print(f"处理历史日志点击事件时出错: {e}")

    def Show_Context_Menu(self, position):
        """显示右键菜单"""
        item = self.listWidget_historyLogs.itemAt(position)
        if item:
            context_menu = QMenu(self)
            delete_action = QAction("删除此日志文件", self)
            delete_action.triggered.connect(lambda: self.Delete_Single_Log(item))
            context_menu.addAction(delete_action)
            context_menu.exec_(self.listWidget_historyLogs.mapToGlobal(position))

    def _delete_log_files(self, files_to_delete, operation_name="删除"):
        """通用的文件删除方法,减少重复代码"""
        deleted_count = 0
        failed_files = []
        current_date = self.calendarWidget.selectedDate().toString("yyyy-MM-dd")
        need_clear_display = False

        for file_info in files_to_delete:
            try:
                if isinstance(file_info, tuple):
                    logfile, date_str = file_info
                else:
                    # 处理单个文件删除的情况
                    logfile = file_info.data(Qt.UserRole + 1)
                    date_str = file_info.data(Qt.UserRole)

                if os.path.exists(logfile):
                    os.remove(logfile)
                    deleted_count += 1

                    # 检查是否需要清空当前显示
                    if date_str == current_date:
                        need_clear_display = True
                else:
                    failed_files.append(f'{date_str} (文件不存在)')

            except Exception as e:
                failed_files.append(f'{date_str} (删除失败: {str(e)})')

        # 重新加载列表
        self.Load_History_Log_List()

        # 清空当前显示(如果需要)
        if need_clear_display:
            self.current_log_content = ""
            self.plainTextEdit_log.clear()
            self.Update_Log_Types()

        # 更新日历显示
        self.Set_Log_Date()

        return deleted_count, failed_files

    def Delete_Single_Log(self, item):
        """删除单个日志文件"""
        try:
            date = item.data(Qt.UserRole)
            logfile = item.data(Qt.UserRole + 1)

            # 确认删除
            reply = QMessageBox.question(self, '确认删除', 
                                       f'确定要删除日志文件 "{date}" 吗?\n\n文件路径: {logfile}\n\n此操作不可撤销!',
                                       QMessageBox.Yes | QMessageBox.No,
                                       QMessageBox.No)

            if reply == QMessageBox.Yes:
                deleted_count, failed_files = self._delete_log_files([item])

                if failed_files:
                    QMessageBox.warning(self, '删除失败', f'文件不存在: {logfile}')
                else:
                    QMessageBox.information(self, '删除成功', f'日志文件 "{date}" 已删除')

        except Exception as e:
            QMessageBox.critical(self, '删除失败', f'删除文件时出错: {str(e)}')

    def Delete_Selected_Logs(self):
        """批量删除选中的日志文件"""
        try:
            # 在批量模式下,检查复选框状态
            selected_items = []
            for i in range(self.listWidget_historyLogs.count()):
                item = self.listWidget_historyLogs.item(i)
                if item.checkState() == Qt.Checked:
                    selected_items.append(item)

            if not selected_items:
                QMessageBox.information(self, '提示', '请先勾选要删除的日志文件')
                return

            # 确认删除
            file_list = [item.data(Qt.UserRole) for item in selected_items]
            reply = QMessageBox.question(self, '确认批量删除', 
                                       f'确定要删除以下 {len(selected_items)} 个日志文件吗?\n\n' + 
                                       '\n'.join(file_list) + 
                                       '\n\n此操作不可撤销!',
                                       QMessageBox.Yes | QMessageBox.No,
                                       QMessageBox.No)

            if reply == QMessageBox.Yes:
                deleted_count, failed_files = self._delete_log_files(selected_items)

                # 显示结果
                if failed_files:
                    QMessageBox.warning(self, '批量删除完成', 
                                       f'成功删除 {deleted_count} 个文件\n\n失败的文件:\n' + 
                                       '\n'.join(failed_files))
                else:
                    QMessageBox.information(self, '批量删除成功', f'成功删除 {deleted_count} 个日志文件')

        except Exception as e:
            QMessageBox.critical(self, '批量删除失败', f'批量删除时出错: {str(e)}')

    def Enter_Batch_Mode(self):
        """进入批量删除模式"""
        self.batch_mode = True

        # 切换按钮显示状态
        self.pushButton_batchMode.setVisible(False)
        self.pushButton_deleteSelected.setVisible(True)
        self.pushButton_cancelBatch.setVisible(True)

        # 重新加载列表以显示复选框
        self.Load_History_Log_List()

        # 禁用右键菜单和单击事件
        self.listWidget_historyLogs.setContextMenuPolicy(Qt.NoContextMenu)
        self.listWidget_historyLogs.itemClicked.disconnect()

    def Exit_Batch_Mode(self):
        """退出批量删除模式"""
        self.batch_mode = False

        # 切换按钮显示状态
        self.pushButton_batchMode.setVisible(True)
        self.pushButton_deleteSelected.setVisible(False)
        self.pushButton_cancelBatch.setVisible(False)

        # 重新加载列表以隐藏复选框
        self.Load_History_Log_List()

        # 恢复右键菜单和单击事件
        self.listWidget_historyLogs.setContextMenuPolicy(Qt.CustomContextMenu)
        self.listWidget_historyLogs.itemClicked.connect(self.On_History_Log_Clicked)

    def Delete_Logs_By_Date(self):
        """按日期范围删除日志文件"""
        try:
            # 获取用户输入的天数
            days, ok = QInputDialog.getInt(self, '按日期删除日志', 
                                          '请输入要删除多少天前的日志文件:\n(例如:输入7表示删除7天前及更早的日志)', 
                                          7, 1, 365, 1)

            if not ok:
                return

            # 计算截止日期
            cutoff_date = datetime.now() - timedelta(days=days)
            cutoff_date_str = cutoff_date.strftime(self.DATE_FORMAT)

            # 查找符合条件的日志文件
            log_files = glob.glob(os.path.join(LOG_FILES, "*.log"))
            files_to_delete = []

            for logfile in log_files:
                try:
                    # 从文件名提取日期
                    filename = os.path.basename(logfile)
                    date_str = filename.split('_')[1].split('.')[0]
                    file_date = datetime.strptime(date_str, self.DATE_FORMAT)

                    # 如果文件日期早于截止日期,加入删除列表
                    if file_date < cutoff_date:
                        files_to_delete.append((logfile, date_str))

                except Exception as e:
                    print(f"解析文件日期时出错 {logfile}: {e}")
                    continue

            if not files_to_delete:
                QMessageBox.information(self, '提示', f'没有找到{days}天前的日志文件')
                return

            # 确认删除
            file_list = [date for _, date in files_to_delete]
            reply = QMessageBox.question(self, '确认按日期删除', 
                                       f'找到 {len(files_to_delete)}{days}天前的日志文件:\n\n' + 
                                       '\n'.join(file_list[:10]) + 
                                       (f'\n... 还有{len(file_list)-10}个文件' if len(file_list) > 10 else '') +
                                       f'\n\n确定要删除{cutoff_date_str}之前的所有日志文件吗?\n此操作不可撤销!',
                                       QMessageBox.Yes | QMessageBox.No,
                                       QMessageBox.No)

            if reply == QMessageBox.Yes:
                deleted_count, failed_files = self._delete_log_files(files_to_delete)

                # 显示结果
                if failed_files:
                    QMessageBox.warning(self, '按日期删除完成', 
                                       f'成功删除 {deleted_count} 个文件\n\n失败的文件:\n' + 
                                       '\n'.join(failed_files))
                else:
                    QMessageBox.information(self, '按日期删除成功', f'成功删除 {deleted_count}{days}天前的日志文件')

        except Exception as e:
            QMessageBox.critical(self, '按日期删除失败', f'按日期删除时出错: {str(e)}')

    def Enter_Batch_Mode(self):
        """进入批量删除模式"""
        self.batch_mode = True

        # 切换按钮显示状态
        self.pushButton_batchMode.setVisible(False)
        self.pushButton_deleteSelected.setVisible(True)
        self.pushButton_cancelBatch.setVisible(True)

        # 重新加载列表以显示复选框
        self.Load_History_Log_List()

        # 禁用右键菜单和单击事件
        self.listWidget_historyLogs.setContextMenuPolicy(Qt.NoContextMenu)
        self.listWidget_historyLogs.itemClicked.disconnect()

    def Exit_Batch_Mode(self):
        """退出批量删除模式"""
        self.batch_mode = False

        # 切换按钮显示状态
        self.pushButton_batchMode.setVisible(True)
        self.pushButton_deleteSelected.setVisible(False)
        self.pushButton_cancelBatch.setVisible(False)

        # 重新加载列表以隐藏复选框
        self.Load_History_Log_List()

        # 恢复右键菜单和单击事件
        self.listWidget_historyLogs.setContextMenuPolicy(Qt.CustomContextMenu)
        self.listWidget_historyLogs.itemClicked.connect(self.On_History_Log_Clicked)

    def Set_Button_Styles(self):
        """设置按钮样式"""
        # 批量模式按钮样式 - 蓝色背景
        batch_style = """
            QPushButton {
                background-color: #3498db;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #2980b9;
            }
            QPushButton:pressed {
                background-color: #21618c;
            }
        """

        # 删除选中按钮样式 - 红色背景
        delete_style = """
            QPushButton {
                background-color: #e74c3c;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #c0392b;
            }
            QPushButton:pressed {
                background-color: #a93226;
            }
        """

        # 取消按钮样式 - 灰色背景
        cancel_style = """
            QPushButton {
                background-color: #95a5a6;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #7f8c8d;
            }
            QPushButton:pressed {
                background-color: #6c7b7d;
            }
        """

        # 按时间删除按钮样式 - 紫色背景
        date_delete_style = """
            QPushButton {
                background-color: #9b59b6;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #8e44ad;
            }
            QPushButton:pressed {
                background-color: #7d3c98;
            }
        """

        # 应用样式
        self.pushButton_deleteSelected.setStyleSheet(delete_style)
        self.pushButton_batchMode.setStyleSheet(batch_style)
        self.pushButton_cancelBatch.setStyleSheet(cancel_style)
        self.pushButton_deleteByDate.setStyleSheet(date_delete_style)


    # ============== 鼠标拖动勾选(批量模式)支持 ==============
    def _apply_drag_check_range(self, start_row: int, end_row: int):
        """将指定范围内的列表项设置为目标勾选状态

        在批量模式下,用户可以通过鼠标拖动来选择多个项目。此方法负责将指定行范围
        内的未处理条目设置为预设的目标勾选状态,避免重复处理已设置的条目。

        Args:
            start_row (int): 起始行号(包含)
            end_row (int): 结束行号(包含)

        Returns:
            None: 无返回值

        Note:
            - 方法会自动处理起始行号大于结束行号的情况
            - 只处理具有复选框功能的条目(Qt.ItemIsUserCheckable)
            - 已处理过的行号会被记录在self._drag_checked_indexes中避免重复处理
        """
        if start_row > end_row:
            start_row, end_row = end_row, start_row
        for row in range(start_row, end_row + 1):
            if row in self._drag_checked_indexes:
                continue
            item = self.listWidget_historyLogs.item(row)
            if not item:
                continue
            # 仅在复选框可用时生效
            if not (item.flags() & Qt.ItemIsUserCheckable):
                continue
            item.setCheckState(self._drag_check_target_state)
            self._drag_checked_indexes.add(row)

    def eventFilter(self, obj, event):
        """事件过滤器,用于在批量模式下支持通过鼠标拖动来批量勾选或取消勾选列表项。

        该方法拦截发送到 listWidget_historyLogs 视口的鼠标事件,并根据鼠标操作实现
        拖动选择功能。仅在 batch_mode 为 True 时启用此功能。

        Args:
            obj (QObject): 发送事件的对象,应为 listWidget_historyLogs 的视口。
            event (QEvent): 具体的事件对象,如 MouseButtonPress、MouseMove 等。

        Returns:
            bool: 如果事件被处理则返回 True,否则调用父类的事件过滤器并返回其结果。
        """
        if obj is self.listWidget_historyLogs.viewport():
            # 仅在批量模式下启用该功能
            if not getattr(self, 'batch_mode', False):
                return super().eventFilter(obj, event)

            et = event.type()
            # 左键按下:开始拖动勾选操作
            if et == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
                pos = event.pos()
                item = self.listWidget_historyLogs.itemAt(pos)
                if item and (item.flags() & Qt.ItemIsUserCheckable):
                    self._drag_check_active = True
                    self._drag_checked_indexes.clear()
                    self._drag_last_row = self.listWidget_historyLogs.row(item)
                    # 目标状态取决于按下时的当前状态(按下第一个条目时切换一次)
                    self._drag_check_target_state = Qt.Checked if item.checkState() != Qt.Checked else Qt.Unchecked
                    item.setCheckState(self._drag_check_target_state)
                    self._drag_checked_indexes.add(self._drag_last_row)
                    return True  # 吞掉事件,避免触发选择行为

            # 鼠标移动:在拖动中对经过的条目应用统一的勾选状态,并处理自动滚动
            if et == QEvent.MouseMove and getattr(self, '_drag_check_active', False):
                pos = event.pos()
                viewport = self.listWidget_historyLogs.viewport()
                h = viewport.height()
                y = pos.y()

                # 自动滚动:靠近顶部/底部时缓慢滚动
                vbar = self.listWidget_historyLogs.verticalScrollBar()
                step = max(1, vbar.singleStep())
                if y < self._drag_autoscroll_margin:
                    vbar.setValue(max(vbar.minimum(), vbar.value() - step))
                elif y > h - self._drag_autoscroll_margin:
                    vbar.setValue(min(vbar.maximum(), vbar.value() + step))

                # 应用行范围切换,避免快速移动遗漏中间项
                item = self.listWidget_historyLogs.itemAt(event.pos())
                if item:
                    cur_row = self.listWidget_historyLogs.row(item)
                    self._apply_drag_check_range(self._drag_last_row, cur_row)
                    self._drag_last_row = cur_row
                return True

            # 左键释放:结束拖动
            if et == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton:
                if getattr(self, '_drag_check_active', False):
                    self._drag_check_active = False
                    self._drag_checked_indexes.clear()
                    return True

            # 鼠标离开视口:结束拖动状态
            if et == QEvent.Leave:
                if getattr(self, '_drag_check_active', False):
                    self._drag_check_active = False
                    self._drag_checked_indexes.clear()
                    return True

        return super().eventFilter(obj, event)

Apply_Colors_Simple(content)

简化的颜色渲染模式,用于大量日志 - 使用统一工具类

Source code in src/components/LogViewer/log_viewer_demo.py
def Apply_Colors_Simple(self, content):
    """简化的颜色渲染模式,用于大量日志 - 使用统一工具类"""
    # 使用统一的日志显示工具类的简化模式
    log_display_util._apply_colors_simple(self.plainTextEdit_log, content)

Apply_Colors_To_Content(content)

应用颜色到日志内容 - 使用统一工具类

Source code in src/components/LogViewer/log_viewer_demo.py
def Apply_Colors_To_Content(self, content):
    """应用颜色到日志内容 - 使用统一工具类"""
    # 使用统一的日志显示工具类进行颜色渲染
    log_display_util.apply_colors_to_text_widget(self.plainTextEdit_log, content)

Count_Log_Lines_Fast(filepath)

快速统计日志文件行数

Source code in src/components/LogViewer/log_viewer_demo.py
def Count_Log_Lines_Fast(self, filepath):
    """快速统计日志文件行数"""
    try:
        count = 0
        with open(filepath, 'r', encoding='utf-8') as file:
            # 使用缓冲读取,避免一次性加载整个文件
            buffer_size = 8192
            while True:
                buffer = file.read(buffer_size)
                if not buffer:
                    break
                count += buffer.count('\n')
        return count
    except Exception as e:
        print(f"统计文件 {filepath} 行数时出错: {e}")
        return 0

Delete_Logs_By_Date()

按日期范围删除日志文件

Source code in src/components/LogViewer/log_viewer_demo.py
def Delete_Logs_By_Date(self):
    """按日期范围删除日志文件"""
    try:
        # 获取用户输入的天数
        days, ok = QInputDialog.getInt(self, '按日期删除日志', 
                                      '请输入要删除多少天前的日志文件:\n(例如:输入7表示删除7天前及更早的日志)', 
                                      7, 1, 365, 1)

        if not ok:
            return

        # 计算截止日期
        cutoff_date = datetime.now() - timedelta(days=days)
        cutoff_date_str = cutoff_date.strftime(self.DATE_FORMAT)

        # 查找符合条件的日志文件
        log_files = glob.glob(os.path.join(LOG_FILES, "*.log"))
        files_to_delete = []

        for logfile in log_files:
            try:
                # 从文件名提取日期
                filename = os.path.basename(logfile)
                date_str = filename.split('_')[1].split('.')[0]
                file_date = datetime.strptime(date_str, self.DATE_FORMAT)

                # 如果文件日期早于截止日期,加入删除列表
                if file_date < cutoff_date:
                    files_to_delete.append((logfile, date_str))

            except Exception as e:
                print(f"解析文件日期时出错 {logfile}: {e}")
                continue

        if not files_to_delete:
            QMessageBox.information(self, '提示', f'没有找到{days}天前的日志文件')
            return

        # 确认删除
        file_list = [date for _, date in files_to_delete]
        reply = QMessageBox.question(self, '确认按日期删除', 
                                   f'找到 {len(files_to_delete)}{days}天前的日志文件:\n\n' + 
                                   '\n'.join(file_list[:10]) + 
                                   (f'\n... 还有{len(file_list)-10}个文件' if len(file_list) > 10 else '') +
                                   f'\n\n确定要删除{cutoff_date_str}之前的所有日志文件吗?\n此操作不可撤销!',
                                   QMessageBox.Yes | QMessageBox.No,
                                   QMessageBox.No)

        if reply == QMessageBox.Yes:
            deleted_count, failed_files = self._delete_log_files(files_to_delete)

            # 显示结果
            if failed_files:
                QMessageBox.warning(self, '按日期删除完成', 
                                   f'成功删除 {deleted_count} 个文件\n\n失败的文件:\n' + 
                                   '\n'.join(failed_files))
            else:
                QMessageBox.information(self, '按日期删除成功', f'成功删除 {deleted_count}{days}天前的日志文件')

    except Exception as e:
        QMessageBox.critical(self, '按日期删除失败', f'按日期删除时出错: {str(e)}')

Delete_Selected_Logs()

批量删除选中的日志文件

Source code in src/components/LogViewer/log_viewer_demo.py
def Delete_Selected_Logs(self):
    """批量删除选中的日志文件"""
    try:
        # 在批量模式下,检查复选框状态
        selected_items = []
        for i in range(self.listWidget_historyLogs.count()):
            item = self.listWidget_historyLogs.item(i)
            if item.checkState() == Qt.Checked:
                selected_items.append(item)

        if not selected_items:
            QMessageBox.information(self, '提示', '请先勾选要删除的日志文件')
            return

        # 确认删除
        file_list = [item.data(Qt.UserRole) for item in selected_items]
        reply = QMessageBox.question(self, '确认批量删除', 
                                   f'确定要删除以下 {len(selected_items)} 个日志文件吗?\n\n' + 
                                   '\n'.join(file_list) + 
                                   '\n\n此操作不可撤销!',
                                   QMessageBox.Yes | QMessageBox.No,
                                   QMessageBox.No)

        if reply == QMessageBox.Yes:
            deleted_count, failed_files = self._delete_log_files(selected_items)

            # 显示结果
            if failed_files:
                QMessageBox.warning(self, '批量删除完成', 
                                   f'成功删除 {deleted_count} 个文件\n\n失败的文件:\n' + 
                                   '\n'.join(failed_files))
            else:
                QMessageBox.information(self, '批量删除成功', f'成功删除 {deleted_count} 个日志文件')

    except Exception as e:
        QMessageBox.critical(self, '批量删除失败', f'批量删除时出错: {str(e)}')

Delete_Single_Log(item)

删除单个日志文件

Source code in src/components/LogViewer/log_viewer_demo.py
def Delete_Single_Log(self, item):
    """删除单个日志文件"""
    try:
        date = item.data(Qt.UserRole)
        logfile = item.data(Qt.UserRole + 1)

        # 确认删除
        reply = QMessageBox.question(self, '确认删除', 
                                   f'确定要删除日志文件 "{date}" 吗?\n\n文件路径: {logfile}\n\n此操作不可撤销!',
                                   QMessageBox.Yes | QMessageBox.No,
                                   QMessageBox.No)

        if reply == QMessageBox.Yes:
            deleted_count, failed_files = self._delete_log_files([item])

            if failed_files:
                QMessageBox.warning(self, '删除失败', f'文件不存在: {logfile}')
            else:
                QMessageBox.information(self, '删除成功', f'日志文件 "{date}" 已删除')

    except Exception as e:
        QMessageBox.critical(self, '删除失败', f'删除文件时出错: {str(e)}')

Enter_Batch_Mode()

进入批量删除模式

Source code in src/components/LogViewer/log_viewer_demo.py
def Enter_Batch_Mode(self):
    """进入批量删除模式"""
    self.batch_mode = True

    # 切换按钮显示状态
    self.pushButton_batchMode.setVisible(False)
    self.pushButton_deleteSelected.setVisible(True)
    self.pushButton_cancelBatch.setVisible(True)

    # 重新加载列表以显示复选框
    self.Load_History_Log_List()

    # 禁用右键菜单和单击事件
    self.listWidget_historyLogs.setContextMenuPolicy(Qt.NoContextMenu)
    self.listWidget_historyLogs.itemClicked.disconnect()

Exit_Batch_Mode()

退出批量删除模式

Source code in src/components/LogViewer/log_viewer_demo.py
def Exit_Batch_Mode(self):
    """退出批量删除模式"""
    self.batch_mode = False

    # 切换按钮显示状态
    self.pushButton_batchMode.setVisible(True)
    self.pushButton_deleteSelected.setVisible(False)
    self.pushButton_cancelBatch.setVisible(False)

    # 重新加载列表以隐藏复选框
    self.Load_History_Log_List()

    # 恢复右键菜单和单击事件
    self.listWidget_historyLogs.setContextMenuPolicy(Qt.CustomContextMenu)
    self.listWidget_historyLogs.itemClicked.connect(self.On_History_Log_Clicked)

Filter_Log_By_Type(log_type)

根据日志类型过滤日志内容并应用颜色

Source code in src/components/LogViewer/log_viewer_demo.py
def Filter_Log_By_Type(self, log_type):
    """根据日志类型过滤日志内容并应用颜色"""
    if not self.current_log_content:
        return

    self.plainTextEdit_log.clear()

    if log_type == "全部":
        # 显示所有日志并应用颜色
        self.Apply_Colors_To_Content(self.current_log_content)
    else:
        # 使用统一工具类按类型过滤日志并应用颜色
        filtered_content = log_display_util.filter_logs_by_level(self.current_log_content, log_type)
        self.Apply_Colors_To_Content(filtered_content)

Load_History_Log_List()

加载历史日志文件列表 - 优化版本

Source code in src/components/LogViewer/log_viewer_demo.py
def Load_History_Log_List(self):
    """加载历史日志文件列表 - 优化版本"""
    try:
        self.listWidget_historyLogs.clear()
        log_files = glob.glob(os.path.join(LOG_FILES, "*.log"))
        log_files.sort()  # 按文件名排序

        # 先快速加载文件名,后续异步加载详细信息
        for logfile in log_files:
            try:
                # 提取日期信息
                date = os.path.basename(logfile).split('_')[1].split('.')[0]

                # 获取文件大小作为快速指标
                file_size = os.path.getsize(logfile)
                size_kb = file_size // 1024

                # 创建显示文本(先显示文件大小,避免读取文件内容)
                display_text = f"{date} ({size_kb}KB)"

                # 添加到列表控件
                item = QListWidgetItem(display_text)
                item.setData(Qt.UserRole, date)  # 存储日期信息
                item.setData(Qt.UserRole + 1, logfile)  # 存储文件路径

                # 根据批量模式设置复选框
                if self.batch_mode:
                    item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                    item.setCheckState(Qt.Unchecked)
                else:
                    item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable)

                self.listWidget_historyLogs.addItem(item)

            except Exception as e:
                print(f"处理文件 {logfile} 时出错: {e}")

        # 异步更新详细信息(行数统计)
        QtCore.QTimer.singleShot(100, self.Update_Log_Counts_Async)

    except Exception as e:
        print(f"加载历史日志列表时出错: {e}")

On_History_Log_Clicked(item)

处理历史日志列表点击事件

Source code in src/components/LogViewer/log_viewer_demo.py
def On_History_Log_Clicked(self, item):
    """处理历史日志列表点击事件"""
    try:
        # 获取存储的日期信息
        date = item.data(Qt.UserRole)
        if date:
            # 临时断开日历信号连接,避免循环触发
            self.calendarWidget.selectionChanged.disconnect()

            # 设置日历控件到对应日期
            selected_date = QDate.fromString(date, "yyyy-MM-dd")
            if selected_date.isValid():
                self.calendarWidget.setSelectedDate(selected_date)

            # 重新连接信号
            self.calendarWidget.selectionChanged.connect(lambda:self.Get_Log_File_By_Date(self.calendarWidget.selectedDate().toString("yyyy-MM-dd")))

            # 加载对应日期的日志
            self.Get_Log_File_By_Date(date)
    except Exception as e:
        print(f"处理历史日志点击事件时出错: {e}")

Set_Button_Styles()

设置按钮样式

Source code in src/components/LogViewer/log_viewer_demo.py
def Set_Button_Styles(self):
    """设置按钮样式"""
    # 批量模式按钮样式 - 蓝色背景
    batch_style = """
        QPushButton {
            background-color: #3498db;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            font-weight: bold;
        }
        QPushButton:hover {
            background-color: #2980b9;
        }
        QPushButton:pressed {
            background-color: #21618c;
        }
    """

    # 删除选中按钮样式 - 红色背景
    delete_style = """
        QPushButton {
            background-color: #e74c3c;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            font-weight: bold;
        }
        QPushButton:hover {
            background-color: #c0392b;
        }
        QPushButton:pressed {
            background-color: #a93226;
        }
    """

    # 取消按钮样式 - 灰色背景
    cancel_style = """
        QPushButton {
            background-color: #95a5a6;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            font-weight: bold;
        }
        QPushButton:hover {
            background-color: #7f8c8d;
        }
        QPushButton:pressed {
            background-color: #6c7b7d;
        }
    """

    # 按时间删除按钮样式 - 紫色背景
    date_delete_style = """
        QPushButton {
            background-color: #9b59b6;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            font-weight: bold;
        }
        QPushButton:hover {
            background-color: #8e44ad;
        }
        QPushButton:pressed {
            background-color: #7d3c98;
        }
    """

    # 应用样式
    self.pushButton_deleteSelected.setStyleSheet(delete_style)
    self.pushButton_batchMode.setStyleSheet(batch_style)
    self.pushButton_cancelBatch.setStyleSheet(cancel_style)
    self.pushButton_deleteByDate.setStyleSheet(date_delete_style)

Show_Context_Menu(position)

显示右键菜单

Source code in src/components/LogViewer/log_viewer_demo.py
def Show_Context_Menu(self, position):
    """显示右键菜单"""
    item = self.listWidget_historyLogs.itemAt(position)
    if item:
        context_menu = QMenu(self)
        delete_action = QAction("删除此日志文件", self)
        delete_action.triggered.connect(lambda: self.Delete_Single_Log(item))
        context_menu.addAction(delete_action)
        context_menu.exec_(self.listWidget_historyLogs.mapToGlobal(position))

Update_Log_Counts_Async()

异步更新日志行数统计

Source code in src/components/LogViewer/log_viewer_demo.py
def Update_Log_Counts_Async(self):
    """异步更新日志行数统计"""
    try:
        for i in range(self.listWidget_historyLogs.count()):
            item = self.listWidget_historyLogs.item(i)
            if item:
                logfile = item.data(Qt.UserRole + 1)
                date = item.data(Qt.UserRole)

                try:
                    # 使用更高效的行数统计方法
                    log_count = self.Count_Log_Lines_Fast(logfile)

                    # 更新显示文本
                    display_text = f"{date} ({log_count}条日志)"
                    item.setText(display_text)

                    # 强制刷新UI
                    QtWidgets.QApplication.processEvents()

                except Exception as e:
                    print(f"更新文件 {logfile} 行数时出错: {e}")

    except Exception as e:
        print(f"异步更新日志行数时出错: {e}")

Update_Log_Types()

动态扫描当前日志内容中的所有日志类型并更新下拉框

Source code in src/components/LogViewer/log_viewer_demo.py
def Update_Log_Types(self):
    """动态扫描当前日志内容中的所有日志类型并更新下拉框"""
    # 保存当前选择的类型
    current_selection = self.comboBox_logType.currentText()

    # 清空下拉框
    self.comboBox_logType.clear()

    # 添加"全部"选项
    self.comboBox_logType.addItem("全部")

    if self.current_log_content:
        # 使用统一工具类提取日志级别
        log_levels = log_display_util.get_log_levels_from_content(self.current_log_content)

        # 按字母顺序排序并添加到下拉框
        for level in sorted(log_levels):
            self.comboBox_logType.addItem(level)

    # 尝试恢复之前的选择,如果不存在则选择"全部"
    index = self.comboBox_logType.findText(current_selection)
    if index >= 0:
        self.comboBox_logType.setCurrentIndex(index)
    else:
        self.comboBox_logType.setCurrentIndex(0)  # 选择"全部"

eventFilter(obj, event)

事件过滤器,用于在批量模式下支持通过鼠标拖动来批量勾选或取消勾选列表项。

该方法拦截发送到 listWidget_historyLogs 视口的鼠标事件,并根据鼠标操作实现 拖动选择功能。仅在 batch_mode 为 True 时启用此功能。

Parameters:

Name Type Description Default
obj QObject

发送事件的对象,应为 listWidget_historyLogs 的视口。

required
event QEvent

具体的事件对象,如 MouseButtonPress、MouseMove 等。

required

Returns:

Name Type Description
bool

如果事件被处理则返回 True,否则调用父类的事件过滤器并返回其结果。

Source code in src/components/LogViewer/log_viewer_demo.py
def eventFilter(self, obj, event):
    """事件过滤器,用于在批量模式下支持通过鼠标拖动来批量勾选或取消勾选列表项。

    该方法拦截发送到 listWidget_historyLogs 视口的鼠标事件,并根据鼠标操作实现
    拖动选择功能。仅在 batch_mode 为 True 时启用此功能。

    Args:
        obj (QObject): 发送事件的对象,应为 listWidget_historyLogs 的视口。
        event (QEvent): 具体的事件对象,如 MouseButtonPress、MouseMove 等。

    Returns:
        bool: 如果事件被处理则返回 True,否则调用父类的事件过滤器并返回其结果。
    """
    if obj is self.listWidget_historyLogs.viewport():
        # 仅在批量模式下启用该功能
        if not getattr(self, 'batch_mode', False):
            return super().eventFilter(obj, event)

        et = event.type()
        # 左键按下:开始拖动勾选操作
        if et == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
            pos = event.pos()
            item = self.listWidget_historyLogs.itemAt(pos)
            if item and (item.flags() & Qt.ItemIsUserCheckable):
                self._drag_check_active = True
                self._drag_checked_indexes.clear()
                self._drag_last_row = self.listWidget_historyLogs.row(item)
                # 目标状态取决于按下时的当前状态(按下第一个条目时切换一次)
                self._drag_check_target_state = Qt.Checked if item.checkState() != Qt.Checked else Qt.Unchecked
                item.setCheckState(self._drag_check_target_state)
                self._drag_checked_indexes.add(self._drag_last_row)
                return True  # 吞掉事件,避免触发选择行为

        # 鼠标移动:在拖动中对经过的条目应用统一的勾选状态,并处理自动滚动
        if et == QEvent.MouseMove and getattr(self, '_drag_check_active', False):
            pos = event.pos()
            viewport = self.listWidget_historyLogs.viewport()
            h = viewport.height()
            y = pos.y()

            # 自动滚动:靠近顶部/底部时缓慢滚动
            vbar = self.listWidget_historyLogs.verticalScrollBar()
            step = max(1, vbar.singleStep())
            if y < self._drag_autoscroll_margin:
                vbar.setValue(max(vbar.minimum(), vbar.value() - step))
            elif y > h - self._drag_autoscroll_margin:
                vbar.setValue(min(vbar.maximum(), vbar.value() + step))

            # 应用行范围切换,避免快速移动遗漏中间项
            item = self.listWidget_historyLogs.itemAt(event.pos())
            if item:
                cur_row = self.listWidget_historyLogs.row(item)
                self._apply_drag_check_range(self._drag_last_row, cur_row)
                self._drag_last_row = cur_row
            return True

        # 左键释放:结束拖动
        if et == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton:
            if getattr(self, '_drag_check_active', False):
                self._drag_check_active = False
                self._drag_checked_indexes.clear()
                return True

        # 鼠标离开视口:结束拖动状态
        if et == QEvent.Leave:
            if getattr(self, '_drag_check_active', False):
                self._drag_check_active = False
                self._drag_checked_indexes.clear()
                return True

    return super().eventFilter(obj, event)