1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-20 16:33:03 +08:00

Compare commits

...

4 Commits

Author SHA1 Message Date
KenanZhu 883859d1f9 feat(TimerTaskManageWidget): 实现重复任务执行与历史记录
- onTimerTaskIsExecuted/onTimerTaskIsError 添加历史记录
- 历史记录包含:execute_time、executed_time、result、duration
- 重复任务执行后自动计算并更新下次执行时间
2026-03-16 21:17:48 +08:00
KenanZhu f37bcf836b feat(TimerTaskAddDialog): 添加重复任务 UI 支持
- UI 添加重复配置控件:复选框、周一到周日复选框
- 新增 onRepeatCheckBoxToggled 槽函数控制日期选择显示
- getTimerTask 支持提取重复配置(日期、时分秒)
- 调用 TimerUtils 计算首次执行时间
- 重构导入语句格式
2026-03-16 21:16:46 +08:00
KenanZhu b0d1c0e99e feat(TimerTask): 新增任务执行历史对话框
- 新增 ALTimerTaskHistoryDialog 显示重复任务执行历史
- 支持查看执行时间、运行结果、运行耗时
- 提供清空历史记录功能
- 表格显示:执行时间、结果、耗时(秒/s)、uuid
2026-03-16 21:15:56 +08:00
KenanZhu 5af6120be8 feat(TimerUtils): 新增重复任务时间计算工具
- 新增 TimerUtils.calculateNextRepeatTime 方法
- 支持基于重复日期和目标时间计算下次执行时间
- 如果当天在重复日期且目标时间未过,则返回今天;否则查找下一个匹配日期
2026-03-16 21:15:15 +08:00
6 changed files with 554 additions and 23 deletions
+53 -11
View File
@@ -12,15 +12,11 @@ import uuid
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Slot, QDateTime
)
from PySide6.QtWidgets import (
QLabel, QDialog, QWidget, QSpinBox,
QHBoxLayout, QGridLayout, QDateTimeEdit
)
from PySide6.QtCore import Slot, QDateTime
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QGridLayout, QDateTimeEdit
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
import utils.TimerUtils as TimerUtils
class ALTimerTaskStatus(Enum):
@@ -43,8 +39,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
super().__init__(parent)
self.setupUi(self)
self.connectSignals()
self.modifyUi()
self.connectSignals()
def modifyUi(
@@ -97,6 +93,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.CancelButton.clicked.connect(self.reject)
self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
def getTimerTask(
@@ -121,7 +118,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value()
)
return {
task_data = {
"name": name,
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(),
@@ -129,9 +126,40 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
"silent": silent,
"add_time": added_time,
"status": ALTimerTaskStatus.PENDING,
"executed": False
"executed": False,
"repeat": self.RepeatCheckBox.isChecked(),
"repeat_records": []
}
if task_data["repeat"]:
repeat_days = []
if self.MonCheckBox.isChecked():
repeat_days.append(0)
if self.TueCheckBox.isChecked():
repeat_days.append(1)
if self.WedCheckBox.isChecked():
repeat_days.append(2)
if self.ThuCheckBox.isChecked():
repeat_days.append(3)
if self.FriCheckBox.isChecked():
repeat_days.append(4)
if self.SatCheckBox.isChecked():
repeat_days.append(5)
if self.SunCheckBox.isChecked():
repeat_days.append(6)
if not repeat_days:
repeat_days = [0, 1, 2, 3, 4, 5, 6]
task_data["repeat_days"] = repeat_days
task_data["repeat_hour"] = execute_time.hour
task_data["repeat_minute"] = execute_time.minute
task_data["repeat_second"] = execute_time.second
task_data["execute_time"] = TimerUtils.calculateNextRepeatTime(
task_data["repeat_days"],
task_data["repeat_hour"],
task_data["repeat_minute"],
task_data["repeat_second"]
)
return task_data
@Slot(int)
def onTimerTypeComboBoxIndexChanged(
@@ -140,4 +168,18 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
):
self.SpecificTimerWidget.setVisible(index == 0)
self.RelativeTimerWidget.setVisible(index == 1)
self.RelativeTimerWidget.setVisible(index == 1)
@Slot(bool)
def onRepeatCheckBoxToggled(
self,
checked: bool
):
self.MonCheckBox.setEnabled(checked)
self.TueCheckBox.setEnabled(checked)
self.WedCheckBox.setEnabled(checked)
self.ThuCheckBox.setEnabled(checked)
self.FriCheckBox.setEnabled(checked)
self.SatCheckBox.setEnabled(checked)
self.SunCheckBox.setEnabled(checked)
+124
View File
@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from datetime import datetime
from PySide6.QtCore import Slot, Qt
from PySide6.QtWidgets import (
QDialog, QTableWidget, QTableWidgetItem,
QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QHeaderView
)
class ALTimerTaskHistoryDialog(QDialog):
def __init__(
self,
parent = None,
task_data: dict = None
):
super().__init__(parent)
self.__task_data = task_data
self.__history = task_data.get("history", [])
self.modifyUi()
def modifyUi(
self
):
self.setWindowTitle("定时任务执行历史 - AutoLibrary")
self.setMinimumSize(600, 400)
MainLayout = QVBoxLayout(self)
InfoLayout = QHBoxLayout()
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 14px;")
InfoLayout.addWidget(TaskNameLabel)
InfoLayout.addStretch()
if self.__task_data.get("repeat", False):
repeat_label = QLabel("重复任务")
repeat_label.setStyleSheet("color: #2294FF; font-weight: bold;")
InfoLayout.addWidget(repeat_label)
MainLayout.addLayout(InfoLayout)
self.HistoryTableWidget = QTableWidget()
self.HistoryTableWidget.setColumnCount(4)
self.HistoryTableWidget.setHorizontalHeaderLabels(["执行时间", "结果", "耗时(秒/s", "uuid"])
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
self.HistoryTableWidget.verticalHeader().setVisible(False)
self.HistoryTableWidget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.HistoryTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self._loadHistory()
MainLayout.addWidget(self.HistoryTableWidget)
ButtonLayout = QHBoxLayout()
ButtonLayout.addStretch()
self.CloseButton = QPushButton("关闭")
self.CloseButton.setFixedSize(80, 25)
self.CloseButton.clicked.connect(self.accept)
self.ClearHistoryButton = QPushButton("清空历史")
self.ClearHistoryButton.setFixedSize(80, 25)
self.ClearHistoryButton.clicked.connect(self._clearHistory)
ButtonLayout.addWidget(self.ClearHistoryButton)
ButtonLayout.addWidget(self.CloseButton)
MainLayout.addLayout(ButtonLayout)
def _loadHistory(
self
):
self.HistoryTableWidget.setRowCount(len(self.__history))
for row, record in enumerate(self.__history):
self._addHistoryRow(row, record)
def _addHistoryRow(
self,
row: int,
record: dict
):
execute_time_str = record.get("execute_time", "")
result = record.get("result", "未知")
duration = record.get("duration", 0)
uuid = record.get("uuid", "")
self.HistoryTableWidget.setItem(row, 0, QTableWidgetItem(execute_time_str))
self.HistoryTableWidget.setItem(row, 1, QTableWidgetItem(result))
duration_item = QTableWidgetItem(f"{duration:.2f}")
duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.HistoryTableWidget.setItem(row, 2, duration_item)
self.HistoryTableWidget.setItem(row, 3, QTableWidgetItem(uuid))
if result == "成功":
self.HistoryTableWidget.item(row, 1).setForeground(Qt.GlobalColor.green)
elif result == "失败":
self.HistoryTableWidget.item(row, 1).setForeground(Qt.GlobalColor.red)
@Slot()
def _clearHistory(
self
):
self.__history.clear()
self.HistoryTableWidget.setRowCount(0)
self.__task_data["history"] = self.__history
def getHistory(
self
) -> list:
return self.__history
+71 -3
View File
@@ -21,11 +21,14 @@ from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox,
QHBoxLayout, QVBoxLayout, QLabel, QPushButton
)
import gui.ALTimerTaskHistoryDialog as ALTimerTaskHistoryDialog
from PySide6.QtGui import (
QCloseEvent
)
import utils.ConfigManager as ConfigManager
import utils.TimerUtils as TimerUtils
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus
@@ -61,9 +64,19 @@ class ALTimerTaskItemWidget(QWidget):
TaskNameLabel.setFont(TaskNameLabelFont)
TaskNameLabel.setFixedHeight(25)
self.TaskInfoLayout.addWidget(TaskNameLabel)
ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
if self.__timer_task.get("repeat", False):
repeat_days = self.__timer_task.get("repeat_days", [])
if len(repeat_days) == 7:
time_str = f"{self.__timer_task.get('repeat_hour', 0):02d}:{self.__timer_task.get('repeat_minute', 0):02d}:{self.__timer_task.get('repeat_second', 0):02d}"
ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每日 {time_str})")
else:
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
selected_days = [day_names[d] for d in repeat_days]
time_str = f"{self.__timer_task.get('repeat_hour', 0):02d}:{self.__timer_task.get('repeat_minute', 0):02d}:{self.__timer_task.get('repeat_second', 0):02d}"
ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每{','.join(selected_days)} {time_str})")
else:
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
ExecuteTimeLabel.setStyleSheet("color: #969696;")
ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
@@ -124,6 +137,10 @@ class ALTimerTaskItemWidget(QWidget):
if self.__timer_task["status"] == ALTimerTaskStatus.READY\
or self.__timer_task["status"] == ALTimerTaskStatus.RUNNING:
self.DeleteButton.setEnabled(False)
if self.__timer_task.get("repeat", False):
self.HistoryButton = QPushButton("历史")
self.HistoryButton.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(self.HistoryButton)
self.setFixedHeight(55)
@@ -334,6 +351,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
widget.DeleteButton.clicked.connect(
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid)
)
if timer_task.get("repeat", False) and hasattr(widget, "HistoryButton"):
widget.HistoryButton.clicked.connect(
lambda _, task = timer_task: self.showTaskHistory(task)
)
item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget)
@@ -392,6 +413,16 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.timerTasksChanged.emit()
def showTaskHistory(
self,
task: dict
):
dialog = ALTimerTaskHistoryDialog.ALTimerTaskHistoryDialog(self, task)
if dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit()
def checkTasks(
self
):
@@ -471,7 +502,32 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = ALTimerTaskStatus.EXECUTED
if task.get("repeat", False):
if "history" not in task:
task["history"] = []
executed_time = datetime.now()
duration = (executed_time - task["execute_time"]).total_seconds()
task["history"].append({
"execute_time": task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
"result": "成功",
"duration": duration,
"uuid": timer_task["task_uuid"]
})
next_time = TimerUtils.calculateNextRepeatTime(
task["repeat_days"],
task["repeat_hour"],
task["repeat_minute"],
task["repeat_second"]
)
if next_time:
task["execute_time"] = next_time
task["status"] = ALTimerTaskStatus.PENDING
task["executed"] = False
else:
task["status"] = ALTimerTaskStatus.EXECUTED
else:
task["status"] = ALTimerTaskStatus.EXECUTED
self.timerTasksChanged.emit()
@Slot(dict)
@@ -482,5 +538,17 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
if task.get("repeat", False):
if "history" not in task:
task["history"] = []
executed_time = datetime.now()
duration = (executed_time - task["execute_time"]).total_seconds()
task["history"].append({
"execute_time": task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
"result": "失败",
"duration": duration,
"uuid": timer_task["task_uuid"]
})
task["status"] = ALTimerTaskStatus.ERROR
self.timerTasksChanged.emit()
+255 -8
View File
@@ -6,20 +6,20 @@
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
<width>350</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
<width>350</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>300</height>
<width>350</width>
<height>500</height>
</size>
</property>
<property name="windowTitle">
@@ -149,8 +149,20 @@
<property name="spacing">
<number>5</number>
</property>
<item row="0" column="0">
<item row="1" column="0">
<widget class="QRadioButton" name="SilentlyRunRadioButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>静默运行</string>
</property>
@@ -168,13 +180,248 @@
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>运行前提示</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="RepeatConfigGroupBox">
<property name="title">
<string>重复运行</string>
</property>
<layout class="QVBoxLayout" name="RepeatConfigLayout" stretch="1,1,1">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QCheckBox" name="RepeatCheckBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>启用重复执行</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重复周期(全选为每日运行):</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="RepeatCheckBoxLayout">
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="3">
<widget class="QCheckBox" name="ThuCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周四</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="WedCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周三</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="MonCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周一</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="TueCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周二</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="FriCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周五</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="SatCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周六</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="SunCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周日</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
@@ -18,7 +18,7 @@
</property>
<property name="maximumSize">
<size>
<width>600</width>
<width>800</width>
<height>400</height>
</size>
</property>
+50
View File
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from datetime import datetime, timedelta
def calculateNextRepeatTime(
repeat_days: list,
hour: int,
minute: int,
second: int
) -> datetime:
"""
Calculate the next repeat time based on repeat days and target time.
This function calculates the next execution time for a repeatable task.
If the current day is in repeat_days and the target time has not passed,
it returns today's target time. Otherwise, it finds the next matching day.
Args:
repeat_days (list): List of weekdays to repeat (0=Monday, 6=Sunday).
hour (int): Target hour (0-23).
minute (int): Target minute (0-59).
second (int): Target second (0-59).
Returns:
datetime: The next repeat execution time.
"""
current_time = datetime.now()
current_weekday = current_time.weekday()
target_time = current_time.replace(hour=hour, minute=minute, second=second, microsecond=0)
if current_weekday in repeat_days:
if target_time > current_time:
return target_time
repeat_days_sorted = sorted(repeat_days)
for day in repeat_days_sorted:
if day > current_weekday:
days_until = day - current_weekday
next_time = target_time + timedelta(days=days_until)
return next_time
days_until = 7 - current_weekday + repeat_days_sorted[0]
next_time = target_time + timedelta(days=days_until)
return next_time