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

Compare commits

...

14 Commits

Author SHA1 Message Date
KenanZhu 577c651ef8 feat(ALMainWindow): 引入对新增定时器任务状态 - 执行失败的处理支持 (#18ae949)
同时,为了统一消息处理,我们将 ALMainWorkers 中的原信号
槽处理的消息逻辑更改为使用继承的 MsgBase 类的 showTrace 方法
2025-12-13 14:27:46 +08:00
KenanZhu 18ae949900 feat(ALTimerTaskWidget): 新增定时器任务状态 - 执行失败 2025-12-13 14:22:28 +08:00
KenanZhu ca9059d1db refactor(AutoLib): 初始化 AutoLib 时,发生错误则抛出异常 2025-12-13 14:21:26 +08:00
KenanZhu ad4deae0c6 fix(ALMainWindow): 修复停止时的按钮状态重置问题
函数更改于(#9255eec)
2025-12-13 14:15:28 +08:00
KenanZhu 55ae4d0d96 feat(ALConfigWidget): 大更新 - 用户树状列表和其它
1. 在这个 commit 中,我们思考了许久,最终决定将现有的
用户管理列表转为树状列表,以解决用户数量增多时,用户的
选择性管理,分组等问题。
2. 同时因为该更改需要重构很多内容,我们也在该 commit
中决定将所有‘系统配置’更换为‘运行配置’,同时文件名称和
内容变量也相应变为‘run’和‘user’。
3. 重构 AutoLib 和 ALMainWorkers 中的配置相关代码,
以适应新的用户树状列表。

当前迭代更新至 v1.0.0-beta.4, 同时,在该版本的 rc
阶段前,我们计划不再发布 beta 阶段相关的 release
2025-12-13 00:07:33 +08:00
KenanZhu 7dcd72939b fix(ALMainWindow): fix the wrong use of function 'setControlButtons' 2025-12-12 23:51:54 +08:00
KenanZhu bfce61f4b4 fix(ALTimerTaskWidget): fix timer tasks list is 'NoneType' when init config file 2025-12-12 23:41:30 +08:00
KenanZhu 60a5699822 refactor(ALConfigWidget): ALConfigWidget is changed into non-modal dialog 2025-12-12 18:59:25 +08:00
KenanZhu aab9565012 fix(*): always show the child window on the center of the parent window and do not overflow the screen 2025-12-09 08:54:45 +08:00
KenanZhu 9255eec9f1 style(ALMainWindow): rename some variables and functions 2025-12-09 08:51:14 +08:00
KenanZhu cff6fd8fc0 feat(ALTimerTaskWidget): timer tasks' data persistence and perpetuation 2025-12-09 08:49:44 +08:00
KenanZhu b129f47b48 chore(ALSeat*): rename SeatFrame, SeatMapTable, SeatMapWidget to ALSeatFrame, ALSeatMapTable, ALSeatMapWidget 2025-12-09 08:46:51 +08:00
KenanZhu 069429be71 refactor(ALAboutDialog): replace hide/show methods with 'exec()' for dialog modal handling 2025-12-09 08:19:25 +08:00
KenanZhu 7d064fc8e7 refactor(ALMainWindow): extract the worker threads to a separate file : ALMainWorkers.py 2025-12-09 08:17:39 +08:00
15 changed files with 1416 additions and 547 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ class MsgBase:
msg: str msg: str
): ):
self._output_queue.put(f"[{self._class_name:<12}] >>> : {msg}") self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}")
def _showTrace( def _showTrace(
@@ -38,7 +38,7 @@ class MsgBase:
): ):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self._output_queue.put(f"{timestamp}-[{self._class_name:<12}] : {msg}") self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
def _waitMsg( def _waitMsg(
+10 -10
View File
@@ -30,7 +30,7 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
def __init__( def __init__(
self, self,
parent=None parent = None
): ):
super().__init__(parent) super().__init__(parent)
@@ -67,21 +67,21 @@ Version: {AL_VERSION}<br>
Python version: {platform.python_version()}<br> Python version: {platform.python_version()}<br>
Qt version: {self.getQtVersion()}<br> Qt version: {self.getQtVersion()}<br>
<h4>Author Information:</h4> <h4>System Information:</h4>
Developer: KenanZhu<br> Processor: {platform.processor()}<br>
Contact: nanoki_zh@163.com<br> Operating system: {os_info['system']}<br>
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><br> System version: {os_info['version']}<br>
System architecture: {os_info['architecture']}<br>
<h4>Project Information:</h4> <h4>Project Information:</h4>
License: MIT License<br> License: MIT License<br>
Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br> Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
Project website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br> Project website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br>
<h4>System Information:</h4> <h4>Author Information:</h4>
Processor: {platform.processor()}<br> Developer: KenanZhu<br>
Operating system: {os_info['system']}<br> Contact: nanoki_zh@163.com<br>
System version: {os_info['version']}<br> GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><br>
System architecture: {os_info['architecture']}<br>
""" """
return about_text return about_text
+40 -5
View File
@@ -6,25 +6,28 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>300</width> <width>400</width>
<height>300</height> <height>400</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>300</width> <width>400</width>
<height>300</height> <height>400</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>800</width> <width>800</width>
<height>300</height> <height>400</height>
</size> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>关于 - AutoLibrary</string> <string>关于 - AutoLibrary</string>
</property> </property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="ALAboutDialogLayout"> <layout class="QVBoxLayout" name="ALAboutDialogLayout">
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
@@ -95,6 +98,36 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="AboutInfoLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="minimumSize">
<size>
<width>56</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>56</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item> <item>
<widget class="QTextEdit" name="AboutInfoEdit"> <widget class="QTextEdit" name="AboutInfoEdit">
<property name="font"> <property name="font">
@@ -114,6 +147,8 @@
</property> </property>
</widget> </widget>
</item> </item>
</layout>
</item>
<item> <item>
<widget class="QPushButton" name="CopyButton"> <widget class="QPushButton" name="CopyButton">
<property name="minimumSize"> <property name="minimumSize">
+2
View File
@@ -31,10 +31,12 @@ from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog
class TimerTaskStatus(Enum): class TimerTaskStatus(Enum):
PENDING = "等待中" PENDING = "等待中"
READY = "已就绪" READY = "已就绪"
RUNNING = "执行中" RUNNING = "执行中"
EXECUTED = "已执行" EXECUTED = "已执行"
ERROR = "执行失败"
OUTDATED = "已过期" OUTDATED = "已过期"
+508 -256
View File
File diff suppressed because it is too large Load Diff
+95 -37
View File
@@ -93,11 +93,26 @@
<string>用户列表</string> <string>用户列表</string>
</property> </property>
<layout class="QVBoxLayout" name="UserListLayout"> <layout class="QVBoxLayout" name="UserListLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item> <item>
<widget class="QListWidget" name="UserListWidget"> <widget class="QTreeWidget" name="UserTreeWidget">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>230</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
@@ -107,18 +122,58 @@
<height>16777215</height> <height>16777215</height>
</size> </size>
</property> </property>
<property name="isWrapping" stdset="0"> <property name="sizeAdjustPolicy">
<bool>false</bool> <enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored</enum>
</property> </property>
<property name="viewMode"> <property name="tabKeyNavigation">
<enum>QListView::ViewMode::ListMode</enum> <bool>true</bool>
</property> </property>
<property name="currentRow"> <property name="dragEnabled">
<number>-1</number> <bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDropMode::DragDrop</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::DropAction::MoveAction</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property> </property>
<property name="sortingEnabled"> <property name="sortingEnabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="animated">
<bool>true</bool>
</property>
<property name="allColumnsShowFocus">
<bool>false</bool>
</property>
<property name="headerHidden">
<bool>false</bool>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="headerCascadingSectionResizes">
<bool>false</bool>
</attribute>
<attribute name="headerHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="headerShowSortIndicator" stdset="0">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">分组/用户</string>
</property>
</column>
<column>
<property name="text">
<string>状态</string>
</property>
</column>
</widget> </widget>
</item> </item>
<item> <item>
@@ -236,7 +291,7 @@
<widget class="QLabel" name="PasswordLabel"> <widget class="QLabel" name="PasswordLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -325,7 +380,7 @@
<widget class="QLabel" name="UsernameKabel"> <widget class="QLabel" name="UsernameKabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -416,7 +471,7 @@
<widget class="QLabel" name="RoomLabel"> <widget class="QLabel" name="RoomLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -435,7 +490,7 @@
<widget class="QLabel" name="FloorLabel"> <widget class="QLabel" name="FloorLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -454,7 +509,7 @@
<widget class="QLabel" name="ExpectRenewDurationLabel"> <widget class="QLabel" name="ExpectRenewDurationLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -473,7 +528,7 @@
<widget class="QLabel" name="EndTimeLabel"> <widget class="QLabel" name="EndTimeLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>30</height> <height>30</height>
</size> </size>
</property> </property>
@@ -556,7 +611,7 @@
<widget class="QLabel" name="SeatIDLabel"> <widget class="QLabel" name="SeatIDLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -575,7 +630,7 @@
<widget class="QLabel" name="ExpectDurationLabel"> <widget class="QLabel" name="ExpectDurationLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -594,7 +649,7 @@
<widget class="QLabel" name="DateLabel"> <widget class="QLabel" name="DateLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -738,7 +793,7 @@
</time> </time>
</property> </property>
<property name="displayFormat"> <property name="displayFormat">
<string>H:mm</string> <string>HH:mm</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -746,7 +801,7 @@
<widget class="QLabel" name="PlaceLabel"> <widget class="QLabel" name="PlaceLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -869,6 +924,9 @@
<property name="stepType"> <property name="stepType">
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum> <enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
</property> </property>
<property name="value">
<number>0</number>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@@ -902,7 +960,7 @@
<widget class="QLabel" name="MaxBeginTimeDiffLabel"> <widget class="QLabel" name="MaxBeginTimeDiffLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -921,7 +979,7 @@
<widget class="QLabel" name="MaxEndTimeDiffLabel"> <widget class="QLabel" name="MaxEndTimeDiffLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -965,7 +1023,7 @@
</time> </time>
</property> </property>
<property name="displayFormat"> <property name="displayFormat">
<string>H:mm</string> <string>HH:mm</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -973,7 +1031,7 @@
<widget class="QLabel" name="BeginTimeLabel"> <widget class="QLabel" name="BeginTimeLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -1024,7 +1082,7 @@
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -1047,9 +1105,9 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="SystemConfigWidget"> <widget class="QWidget" name="RunConfigWidget">
<attribute name="title"> <attribute name="title">
<string>系统设置</string> <string>运行设置</string>
</attribute> </attribute>
<layout class="QGridLayout" name="SystemConfigWidgetLayout"> <layout class="QGridLayout" name="SystemConfigWidgetLayout">
<property name="leftMargin"> <property name="leftMargin">
@@ -1579,7 +1637,7 @@
</property> </property>
<layout class="QGridLayout" name="CurrentConfigLayout"> <layout class="QGridLayout" name="CurrentConfigLayout">
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLineEdit" name="CurrentSystemConfigEdit"> <widget class="QLineEdit" name="CurrentRunConfigEdit">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@@ -1620,7 +1678,7 @@
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="CurrentSystemConfigLabel"> <widget class="QLabel" name="CurrentRunConfigLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@@ -1634,7 +1692,7 @@
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>当前系统配置路径:</string> <string>当前运行配置路径:</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -1658,7 +1716,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QPushButton" name="BrowseCurrentSystemConfigButton"> <widget class="QPushButton" name="BrowseCurrentRunConfigButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>35</width> <width>35</width>
@@ -1721,7 +1779,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLineEdit" name="ExportSystemConfigEdit"> <widget class="QLineEdit" name="ExportRunConfigEdit">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@@ -1737,7 +1795,7 @@
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="ExportSystemConfigLabel"> <widget class="QLabel" name="ExportRunConfigLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@@ -1751,7 +1809,7 @@
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>系统配置导出路径:</string> <string>运行配置导出路径:</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -1794,7 +1852,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QPushButton" name="BrowseExportSystemConfigButton"> <widget class="QPushButton" name="BrowseExportRunConfigButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>35</width> <width>35</width>
@@ -1836,12 +1894,12 @@
</item> </item>
</layout> </layout>
<zorder>ExportUserConfigEdit</zorder> <zorder>ExportUserConfigEdit</zorder>
<zorder>ExportSystemConfigLabel</zorder> <zorder>ExportRunConfigLabel</zorder>
<zorder>BrowseExportUserConfigButton</zorder> <zorder>BrowseExportUserConfigButton</zorder>
<zorder>ExportUserConfigLabel</zorder> <zorder>ExportUserConfigLabel</zorder>
<zorder>BrowseExportSystemConfigButton</zorder> <zorder>BrowseExportRunConfigButton</zorder>
<zorder>ExportConfigButton</zorder> <zorder>ExportConfigButton</zorder>
<zorder>ExportSystemConfigEdit</zorder> <zorder>ExportRunConfigEdit</zorder>
</widget> </widget>
</item> </item>
<item> <item>
+46 -161
View File
@@ -13,7 +13,7 @@ import time
import queue import queue
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread, QUrl, Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QMainWindow, QMenu, QSystemTrayIcon QMainWindow, QMenu, QSystemTrayIcon
@@ -26,130 +26,19 @@ from gui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.ALConfigWidget import ALConfigWidget from gui.ALConfigWidget import ALConfigWidget
from gui.ALTimerTaskWidget import ALTimerTaskWidget from gui.ALTimerTaskWidget import ALTimerTaskWidget
from gui.ALAboutDialog import ALAboutDialog from gui.ALAboutDialog import ALAboutDialog
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
from gui import AutoLibraryResource from gui import AutoLibraryResource
from operators.AutoLib import AutoLib
from utils.ConfigReader import ConfigReader from utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class AutoLibWorker(QThread):
finishedSignal = Signal()
showTraceSignal = Signal(str)
showMsgSignal = Signal(str)
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__()
self.__input_queue = input_queue
self.__output_queue = output_queue
self.__config_paths = config_paths
def checkTimeAvailable(
self,
) -> bool:
current_time = time.strftime("%H:%M", time.localtime())
if current_time >= "23:30" or current_time <= "07:30":
return False
return True
def checkConfigPaths(
self,
) -> bool:
if not all(
os.path.exists(path) for path in self.__config_paths.values()
):
self.showTraceSignal.emit(
"配置文件路径不存在, 请检查配置文件路径是否正确。"
)
return False
return True
def run(
self
):
auto_lib = None
try:
if not self.checkTimeAvailable():
self.showTraceSignal.emit(
"当前时间不在图书馆开放时间内。\n"\
" 请在 07:30 - 23:30 之间尝试"
)
return
if not self.checkConfigPaths():
return
self.showTraceSignal.emit("AutoLibrary 开始运行")
auto_lib = AutoLib(
self.__input_queue,
self.__output_queue,
)
auto_lib.run(
ConfigReader(self.__config_paths["system"]),
ConfigReader(self.__config_paths["users"]),
)
except Exception as e:
self.showTraceSignal.emit(
f"AutoLibrary 运行时发生异常 : {e}"
)
finally:
if auto_lib:
auto_lib.close()
self.showTraceSignal.emit("AutoLibrary 运行结束")
self.finishedSignal.emit()
class TimerTaskWorker(AutoLibWorker):
finishedSignal_TimerWorker = Signal(dict)
def __init__(
self,
timer_task: dict,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__(
input_queue,
output_queue,
config_paths,
)
self.__timer_task = timer_task
self.__stopped = False
def run(
self
):
self.showTraceSignal.emit(
f"定时任务 {self.__timer_task['name']} 开始运行"
)
super().run()
self.showTraceSignal.emit(
f"定时任务 {self.__timer_task['name']} 运行结束"
)
self.finishedSignal_TimerWorker.emit(self.__timer_task)
class ALMainWindow(QMainWindow, Ui_ALMainWindow): class ALMainWindow(QMainWindow, Ui_ALMainWindow):
timerTaskIsRunning = Signal(dict) timerTaskIsRunning = Signal(dict)
timerTaskIsExecuted = Signal(dict) timerTaskIsExecuted = Signal(dict)
timerTaskIsError = Signal(dict)
def __init__( def __init__(
self self
@@ -165,12 +54,12 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
script_path = sys.executable script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir() script_dir = QFileInfo(script_path).absoluteDir()
self.__config_paths = { self.__config_paths = {
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json")), "run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")), "user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
} }
self.__alTimerTaskWidget = None self.__alTimerTaskWidget = None
self.__alConfigWidget = None self.__alConfigWidget = None
self.__alAboutDialog = None
self.__auto_lib_thread = None self.__auto_lib_thread = None
self.__current_timer_task_thread = None self.__current_timer_task_thread = None
self.__is_running_timer_task = False self.__is_running_timer_task = False
@@ -192,14 +81,22 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.ManualAction.triggered.connect(self.onManualActionTriggered) self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered) self.AboutAction.triggered.connect(self.onAboutActionTriggered)
# initialize timer task widget, but not show it
self.__alTimerTaskWidget = ALTimerTaskWidget(self, self.__config_paths["timer_task"])
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
self.timerTaskIsError.connect(self.__alTimerTaskWidget.onTimerTaskIsError)
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed)
self.__alTimerTaskWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered( def onAboutActionTriggered(
self self
): ):
if self.__alAboutDialog is None: about_dialog = ALAboutDialog(self)
self.__alAboutDialog = ALAboutDialog(self) about_dialog.exec()
self.__alAboutDialog.show()
def onManualActionTriggered( def onManualActionTriggered(
@@ -274,8 +171,8 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
event: QCloseEvent event: QCloseEvent
): ):
if self.__timer and self.__timer.isActive(): if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
self.__timer.stop() self.__msg_queue_timer.stop()
if self.__timer_task_timer and self.__timer_task_timer.isActive(): if self.__timer_task_timer and self.__timer_task_timer.isActive():
self.__timer_task_timer.stop() self.__timer_task_timer.stop()
if self.__is_running_timer_task: if self.__is_running_timer_task:
@@ -286,7 +183,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.__alTimerTaskWidget.deleteLater() self.__alTimerTaskWidget.deleteLater()
if self.__alConfigWidget: if self.__alConfigWidget:
self.__alConfigWidget.close() self.__alConfigWidget.close()
self.__alConfigWidget.deleteLater() # the config widget is already deleted in the 'self.onConfigWidgetClosed'
super().closeEvent(event) super().closeEvent(event)
@@ -308,9 +205,9 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self self
): ):
self.__timer = QTimer() self.__msg_queue_timer = QTimer()
self.__timer.timeout.connect(self.pollMsgQueue) self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
self.__timer.start(100) self.__msg_queue_timer.start(100)
def startTimerTaskPolling( def startTimerTaskPolling(
@@ -334,7 +231,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.timerTaskIsRunning.emit(timer_task) self.timerTaskIsRunning.emit(timer_task)
self.__timer_task_timer.stop() self.__timer_task_timer.stop()
self.__is_running_timer_task = True self.__is_running_timer_task = True
self.setControlButtons(False, False, True) self.setControlButtons(True, True, False)
if not timer_task["silent"]: if not timer_task["silent"]:
self.TrayIcon.showMessage( self.TrayIcon.showMessage(
"定时任务 - AutoLibrary", "定时任务 - AutoLibrary",
@@ -350,8 +247,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.__config_paths self.__config_paths
) )
self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished) self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.showTraceSignal.connect(self.showTrace)
self.__current_timer_task_thread.showMsgSignal.connect(self.showMsg)
self.__current_timer_task_thread.start() self.__current_timer_task_thread.start()
except queue.Empty: except queue.Empty:
self.__is_running_timer_task = False self.__is_running_timer_task = False
@@ -361,13 +256,13 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
def setControlButtons( def setControlButtons(
self, self,
config_button_enabled: bool, config_button_enabled: bool,
start_button_enabled: bool, stop_button_enabled: bool,
stop_button_enabled: bool start_button_enabled: bool
): ):
self.ConfigButton.setEnabled(config_button_enabled) self.ConfigButton.setEnabled(config_button_enabled)
self.StartButton.setEnabled(start_button_enabled)
self.StopButton.setEnabled(stop_button_enabled) self.StopButton.setEnabled(stop_button_enabled)
self.StartButton.setEnabled(start_button_enabled)
@Slot() @Slot()
def showMsg( def showMsg(
@@ -375,7 +270,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
msg: str msg: str
): ):
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}") self.__output_queue.put(f"[{self.__class_name:<15}] >>> : {msg}")
@Slot() @Slot()
def showTrace( def showTrace(
@@ -384,7 +279,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
): ):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}") self.__output_queue.put(f"{timestamp}-[{self.__class_name:<15}] : {msg}")
@Slot() @Slot()
def pollMsgQueue( def pollMsgQueue(
@@ -398,7 +293,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
except queue.Empty: except queue.Empty:
pass pass
@Slot() @Slot()
def onTimerTaskWidgetClosed( def onTimerTaskWidgetClosed(
self self
@@ -406,7 +300,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.TimerTaskWidgetButton.setEnabled(True) self.TimerTaskWidgetButton.setEnabled(True)
@Slot(dict) @Slot(dict)
def onConfigWidgetClosed( def onConfigWidgetClosed(
self, self,
@@ -417,13 +310,11 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed) self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater() self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None self.__alConfigWidget = None
self.ConfigButton.setEnabled(True) self.setControlButtons(True, False, True)
self.StartButton.setEnabled(True)
self.StopButton.setEnabled(False)
self.__config_paths = config_paths self.__config_paths = config_paths
@Slot(dict) @Slot(dict)
def onTimerTaskReady( def onTimerTaskIsReady(
self, self,
timer_task: dict timer_task: dict
): ):
@@ -433,39 +324,37 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
@Slot(dict) @Slot(dict)
def onTimerTaskFinished( def onTimerTaskFinished(
self, self,
is_error: bool,
timer_task: dict timer_task: dict
): ):
self.__current_timer_task_thread.wait(1000) self.__current_timer_task_thread.wait(1000)
self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished) self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.showTraceSignal.disconnect(self.showTrace)
self.__current_timer_task_thread.showMsgSignal.disconnect(self.showMsg)
self.__current_timer_task_thread.deleteLater() self.__current_timer_task_thread.deleteLater()
self.__current_timer_task_thread = None self.__current_timer_task_thread = None
self.setControlButtons(True, True, False) self.setControlButtons(True, False, True)
self.__is_running_timer_task = False self.__is_running_timer_task = False
self.__timer_task_timer.start(500) self.__timer_task_timer.start(500)
timer_task["executed"] = True timer_task["executed"] = True
self.TrayIcon.showMessage( self.TrayIcon.showMessage(
"定时任务 - AutoLibrary", "定时任务 - AutoLibrary",
f"\n定时任务 '{timer_task['name']}' 执行完成", f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
QSystemTrayIcon.MessageIcon.Information, QSystemTrayIcon.MessageIcon.Information,
1000 1000
) )
self.showTrace(f"定时任务 {timer_task['name']} 执行完成, uuid: {timer_task['task_uuid']}") self.showTrace(
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}"
)
if not is_error:
self.timerTaskIsExecuted.emit(timer_task) self.timerTaskIsExecuted.emit(timer_task)
else:
self.timerTaskIsError.emit(timer_task)
@Slot() @Slot()
def onTimerTaskWidgetButtonClicked( def onTimerTaskWidgetButtonClicked(
self self
): ):
if self.__alTimerTaskWidget is None:
self.__alTimerTaskWidget = ALTimerTaskWidget(self)
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
self.__alTimerTaskWidget.timerTaskReady.connect(self.onTimerTaskReady)
self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed)
self.__alTimerTaskWidget.setWindowFlags(Qt.Window)
self.__alTimerTaskWidget.show() self.__alTimerTaskWidget.show()
self.__alTimerTaskWidget.raise_() self.__alTimerTaskWidget.raise_()
self.__alTimerTaskWidget.activateWindow() self.__alTimerTaskWidget.activateWindow()
@@ -482,8 +371,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.__config_paths self.__config_paths
) )
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed) self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.setWindowFlags(Qt.Window)
self.__alConfigWidget.setWindowModality(Qt.ApplicationModal)
self.__alConfigWidget.show() self.__alConfigWidget.show()
self.__alConfigWidget.raise_() self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow() self.__alConfigWidget.activateWindow()
@@ -494,7 +381,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self self
): ):
self.setControlButtons(False, False, True) self.setControlButtons(True, True, False)
if self.__auto_lib_thread is None: if self.__auto_lib_thread is None:
self.__auto_lib_thread = AutoLibWorker( self.__auto_lib_thread = AutoLibWorker(
self.__input_queue, self.__input_queue,
@@ -502,8 +389,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.__config_paths self.__config_paths
) )
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked) self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg) self.__auto_lib_thread.finishedWithErrorSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
self.__auto_lib_thread.start() self.__auto_lib_thread.start()
@Slot() @Slot()
@@ -515,12 +401,11 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.showTrace("正在停止操作......") self.showTrace("正在停止操作......")
self.__auto_lib_thread.wait(2000) self.__auto_lib_thread.wait(2000)
self.showTrace("操作已停止") self.showTrace("操作已停止")
self.__auto_lib_thread.showMsgSignal.disconnect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.disconnect(self.showTrace)
self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked) self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.finishedWithErrorSignal.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.deleteLater() self.__auto_lib_thread.deleteLater()
self.__auto_lib_thread = None self.__auto_lib_thread = None
self.setControlButtons(True, True, False) self.setControlButtons(True, False, True)
@Slot() @Slot()
def onSendButtonClicked( def onSendButtonClicked(
+170
View File
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 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.
"""
import os
import time
import queue
from PySide6.QtCore import (
Slot, Signal, QThread
)
from base.MsgBase import MsgBase
from operators.AutoLib import AutoLib
from utils.ConfigReader import ConfigReader
class AutoLibWorker(QThread, MsgBase):
finishedSignal = Signal()
finishedWithErrorSignal = Signal()
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__(input_queue = input_queue, output_queue = output_queue)
self.__config_paths = config_paths
def checkTimeAvailable(
self,
) -> bool:
current_time = time.strftime("%H:%M", time.localtime())
if current_time >= "23:30" or current_time <= "07:30":
self._showTrace(
"当前时间不在图书馆开放时间内\n"\
" 请在 07:30 - 23:30 之间尝试"
)
return False
return True
def checkConfigPaths(
self,
) -> bool:
if not all(
os.path.exists(path) for path in self.__config_paths.values()
):
self._showTrace("配置文件路径不存在, 请检查配置文件路径是否正确。")
return False
return True
def loadConfigs(
self
) -> bool:
self._showTrace(
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
)
self.__run_config = ConfigReader(
self.__config_paths["run"]
).getConfigs()
self._showTrace(
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
)
self.__user_config = ConfigReader(
self.__config_paths["user"]
).getConfigs()
if self.__run_config is None or self.__user_config is None:
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
return False
if not self.__user_config.get("groups"):
self._showTrace(
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确"
)
return False
return True
def run(
self
):
auto_lib = None
try:
if not self.checkTimeAvailable():
return
if not self.checkConfigPaths():
return
self._showTrace("AutoLibrary 开始运行")
if not self.loadConfigs():
raise Exception("配置文件加载失败")
auto_lib = AutoLib(
self._input_queue,
self._output_queue,
self.__run_config
)
groups = self.__user_config.get("groups")
for group in groups:
if not group["enabled"]:
self._showTrace(f"任务组 {group["name"]} 已跳过")
continue
self._showTrace(f"正在运行任务组 {group["name"]}")
auto_lib.run(
{ "users": group.get("users", []) }
)
except Exception as e:
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
self.finishedWithErrorSignal.emit()
return
if auto_lib:
auto_lib.close()
self._showTrace("AutoLibrary 运行结束")
self.finishedSignal.emit()
class TimerTaskWorker(AutoLibWorker):
finishedSignal_TimerWorker = Signal(bool, dict)
def __init__(
self,
timer_task: dict,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__(input_queue, output_queue, config_paths)
self.__timer_task = timer_task
self.finishedSignal.connect(self.onTimerTaskIsFinished)
self.finishedWithErrorSignal.connect(self.onTimerTaskIsError)
def run(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行")
super().run()
@Slot(dict)
def onTimerTaskIsError(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
self.finishedSignal_TimerWorker.emit(True, self.__timer_task)
@Slot(dict)
def onTimerTaskIsFinished(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.finishedSignal_TimerWorker.emit(False, self.__timer_task)
@@ -15,7 +15,7 @@ from PySide6.QtWidgets import (
) )
class SeatFrame(QFrame): class ALSeatFrame(QFrame):
clicked = Signal(str) clicked = Signal(str)
@@ -18,10 +18,10 @@ from PySide6.QtWidgets import (
from PySide6.QtGui import ( from PySide6.QtGui import (
QPainter, QWheelEvent, QCloseEvent QPainter, QWheelEvent, QCloseEvent
) )
from gui.SeatFrame import SeatFrame from gui.ALSeatFrame import ALSeatFrame
class SeatMapWidget(QWidget): class ALSeatMapWidget(QWidget):
seatMapWidgetClosed = Signal(list) seatMapWidgetClosed = Signal(list)
@@ -115,6 +115,30 @@ class SeatMapWidget(QWidget):
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
@@ -160,7 +184,7 @@ class SeatMapWidget(QWidget):
seats_number = [seat.strip() for seat in row.split(",")] seats_number = [seat.strip() for seat in row.split(",")]
for seat_number in seats_number: for seat_number in seats_number:
if seat_number: if seat_number:
seat_widget = SeatFrame(seat_number) seat_widget = ALSeatFrame(seat_number)
seat_widget.clicked.connect(self.onSeatClicked) seat_widget.clicked.connect(self.onSeatClicked)
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx) self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
self.__seat_frames[seat_number] = seat_widget self.__seat_frames[seat_number] = seat_widget
+199 -21
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
import os import os
import sys import sys
import time import time
import copy
import queue import queue
from enum import Enum from enum import Enum
@@ -23,12 +24,22 @@ from PySide6.QtWidgets import (
QHBoxLayout, QVBoxLayout, QLabel, QPushButton QHBoxLayout, QVBoxLayout, QLabel, QPushButton
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent QCloseEvent, QScreen
) )
from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget
from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus
from utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class SortPolicy(Enum):
BY_NAME = "按名称"
BY_ADD_TIME = "按添加时间"
BY_EXECUTE_TIME = "按执行时间"
class TimerTaskItemWidget(QWidget): class TimerTaskItemWidget(QWidget):
@@ -83,6 +94,9 @@ class TimerTaskItemWidget(QWidget):
case TimerTaskStatus.EXECUTED: case TimerTaskStatus.EXECUTED:
TaskStatusText = "已执行" TaskStatusText = "已执行"
TaskStatusColor = "#4CAF50" TaskStatusColor = "#4CAF50"
case TimerTaskStatus.ERROR:
TaskStatusText = "执行失败"
TaskStatusColor = "#FF5722"
case TimerTaskStatus.OUTDATED: case TimerTaskStatus.OUTDATED:
TaskStatusText = "已过期" TaskStatusText = "已过期"
TaskStatusColor = "#FF5722" TaskStatusColor = "#FF5722"
@@ -125,22 +139,38 @@ class TimerTaskItemWidget(QWidget):
class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget): class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
timerTasksChanged = Signal(list) timerTasksChanged = Signal()
timerTaskReady = Signal(dict) timerTaskIsReady = Signal(dict)
timerTaskWidgetClosed = Signal() timerTaskWidgetClosed = Signal()
def __init__( def __init__(
self, self,
parent = None parent = None,
timer_tasks_config_path: str = ""
): ):
super().__init__(parent) super().__init__(parent)
self.__timer_tasks = [] self.__timer_tasks = []
self.__check_timer = None self.__check_timer = None
self.__sort_policy = SortPolicy.BY_EXECUTE_TIME
self.__timer_tasks_config_path = timer_tasks_config_path
self.setupUi(self) self.setupUi(self)
self.connectSignals() self.connectSignals()
self.setupTimer() self.setupTimer()
if not self.initializeTimerTasks():
return
def connectSignals(
self
):
self.AddTimerTaskButton.clicked.connect(self.addTask)
self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks)
self.TimerTaskSortTypeComboBox.currentIndexChanged.connect(self.onSortPolicyComboBoxChanged)
self.timerTasksChanged.connect(self.onTimerTasksChanged)
def setupTimer( def setupTimer(
@@ -152,12 +182,104 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
self.__check_timer.start(500) self.__check_timer.start(500)
def connectSignals( def initializeTimerTasks(
self self
) -> bool:
timer_tasks = self.loadTimerTasks(self.__timer_tasks_config_path)
if timer_tasks is not None:
self.__timer_tasks = timer_tasks
self.timerTasksChanged.emit()
return True
timer_tasks = []
if self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(timer_tasks)):
QMessageBox.information(
self,
"信息 - AutoLibrary",
f"定时任务配置文件初始化完成: \n{self.__timer_tasks_config_path}"
)
self.__timer_tasks = timer_tasks
self.updateTimerTaskList()
return True
return False
def loadTimerTasks(
self,
timer_tasks_config_path: str
) -> list:
try:
if not timer_tasks_config_path or not os.path.exists(timer_tasks_config_path):
raise Exception("定时任务配置文件不存在")
timer_tasks = ConfigReader(timer_tasks_config_path).getConfigs()
if timer_tasks and "timer_tasks" in timer_tasks:
for task in timer_tasks["timer_tasks"]:
task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
task["status"] = TimerTaskStatus(task["status"])
return timer_tasks["timer_tasks"]
raise Exception("定时任务配置文件格式错误")
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"加载定时任务配置发生错误 ! : {e}\n"\
f"文件路径: {timer_tasks_config_path}"
)
return None
def saveTimerTasks(
self,
timer_tasks_config_path: str,
timer_tasks: list
) -> bool:
try:
if not timer_tasks_config_path:
raise Exception("配置文件路径为空")
for task in timer_tasks:
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S")
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
task["status"] = task["status"].value
ConfigWriter(
timer_tasks_config_path,
{ "timer_tasks": timer_tasks }
)
return True
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"保存定时任务配置发生错误 ! : {e}\n"\
f"文件路径: {timer_tasks_config_path}"
)
return False
def showEvent(
self,
event
): ):
self.AddTimerTaskButton.clicked.connect(self.addTask) result = super().showEvent(event)
self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent( def closeEvent(
@@ -170,6 +292,25 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
event.ignore() event.ignore()
def sortTimerTasks(
self,
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME
):
if policy == SortPolicy.BY_NAME:
self.__timer_tasks.sort(
key = lambda x: x["name"]
)
elif policy == SortPolicy.BY_ADD_TIME:
self.__timer_tasks.sort(
key = lambda x: x["add_time"]
)
elif policy == SortPolicy.BY_EXECUTE_TIME:
self.__timer_tasks.sort(
key = lambda x: x["execute_time"]
)
def updateStat( def updateStat(
self self
): ):
@@ -177,6 +318,7 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
pending = 0 pending = 0
in_queue = 0 in_queue = 0
executed = 0 executed = 0
invalid = 0
total = len(self.__timer_tasks) total = len(self.__timer_tasks)
for timer_task in self.__timer_tasks: for timer_task in self.__timer_tasks:
if timer_task["status"] == TimerTaskStatus.PENDING: if timer_task["status"] == TimerTaskStatus.PENDING:
@@ -186,10 +328,14 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
in_queue += 1 in_queue += 1
elif timer_task["status"] == TimerTaskStatus.EXECUTED: elif timer_task["status"] == TimerTaskStatus.EXECUTED:
executed += 1 executed += 1
elif timer_task["status"] == TimerTaskStatus.ERROR\
or timer_task["status"] == TimerTaskStatus.OUTDATED:
invalid += 1
self.TotalTaskLabel.setText(f"总任务:{total}") self.TotalTaskLabel.setText(f"总任务:{total}")
self.PendingTaskLabel.setText(f"待执行:{pending}") self.PendingTaskLabel.setText(f"待执行:{pending}")
self.InQueueTaskLabel.setText(f"队列中:{in_queue}") self.InQueueTaskLabel.setText(f"队列中:{in_queue}")
self.ExecutedTaskLabel.setText(f"已执行:{executed}") self.ExecutedTaskLabel.setText(f"已执行:{executed}")
self.InvalidTaskLabel.setText(f"无效的:{invalid}")
def updateTimerTaskList( def updateTimerTaskList(
@@ -197,9 +343,7 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
): ):
self.TimerTasksListWidget.clear() self.TimerTasksListWidget.clear()
self.__timer_tasks.sort( self.sortTimerTasks(self.__sort_policy)
key = lambda x: x["execute_time"]
)
for timer_task in self.__timer_tasks: for timer_task in self.__timer_tasks:
item = QListWidgetItem() item = QListWidgetItem()
item.setData(Qt.UserRole, timer_task) item.setData(Qt.UserRole, timer_task)
@@ -220,8 +364,7 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
if dialog.exec() == QDialog.DialogCode.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = dialog.getTimerTask() timer_task = dialog.getTimerTask()
self.__timer_tasks.append(timer_task) self.__timer_tasks.append(timer_task)
self.updateTimerTaskList() self.timerTasksChanged.emit()
self.updateStat()
def deleteTask( def deleteTask(
@@ -233,8 +376,7 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
x for x in self.__timer_tasks x for x in self.__timer_tasks
if x["task_uuid"] != task_uuid if x["task_uuid"] != task_uuid
] ]
self.updateTimerTaskList() self.timerTasksChanged.emit()
self.updateStat()
def clearAllTasks( def clearAllTasks(
@@ -264,14 +406,15 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !" "存在正在执行或已就绪的队列任务,无法清除所有定时任务 !"
) )
self.__timer_tasks = in_queue_tasks self.__timer_tasks = in_queue_tasks
self.updateTimerTaskList() self.timerTasksChanged.emit()
self.updateStat()
def checkTasks( def checkTasks(
self self
): ):
need_update = False
now = datetime.now() now = datetime.now()
for timer_task in self.__timer_tasks: for timer_task in self.__timer_tasks:
if timer_task["execute_time"] > now: if timer_task["execute_time"] > now:
@@ -280,9 +423,35 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
continue continue
if timer_task["execute_time"] <= now + timedelta(seconds = -5): if timer_task["execute_time"] <= now + timedelta(seconds = -5):
timer_task["status"] = TimerTaskStatus.OUTDATED timer_task["status"] = TimerTaskStatus.OUTDATED
need_update = True
else: else:
timer_task["status"] = TimerTaskStatus.READY timer_task["status"] = TimerTaskStatus.READY
self.timerTaskReady.emit(timer_task) self.timerTaskIsReady.emit(timer_task)
need_update = True
if need_update:
self.timerTasksChanged.emit()
@Slot(int)
def onSortPolicyComboBoxChanged(
self,
policy: int
):
mapping = {
0: SortPolicy.BY_NAME,
1: SortPolicy.BY_ADD_TIME,
2: SortPolicy.BY_EXECUTE_TIME
}
self.__sort_policy = mapping[policy]
self.updateTimerTaskList()
@Slot()
def onTimerTasksChanged(
self
):
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks))
self.updateTimerTaskList() self.updateTimerTaskList()
self.updateStat() self.updateStat()
@@ -296,8 +465,7 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.RUNNING task["status"] = TimerTaskStatus.RUNNING
self.updateTimerTaskList() self.timerTasksChanged.emit()
self.updateStat()
@Slot(dict) @Slot(dict)
@@ -309,5 +477,15 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.EXECUTED task["status"] = TimerTaskStatus.EXECUTED
self.updateTimerTaskList() self.timerTasksChanged.emit()
self.updateStat()
@Slot(dict)
def onTimerTaskIsError(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.ERROR
self.timerTasksChanged.emit()
+100
View File
@@ -137,6 +137,30 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="InvalidTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>70</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #FF5722
}</string>
</property>
<property name="text">
<string>无效的:0</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QFrame" name="TimerTaskSpaceFrame"> <widget class="QFrame" name="TimerTaskSpaceFrame">
<property name="minimumSize"> <property name="minimumSize">
@@ -161,6 +185,82 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="TimerTaskSortLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="TimerTaskSortSpaceFrame">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="TimerTaskSortLabel">
<property name="minimumSize">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>排序:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="TimerTaskSortTypeComboBox">
<property name="minimumSize">
<size>
<width>90</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>90</width>
<height>25</height>
</size>
</property>
<item>
<property name="text">
<string>按名称</string>
</property>
</item>
<item>
<property name="text">
<string>按添加时间</string>
</property>
</item>
<item>
<property name="text">
<string>按执行时间</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item> <item>
<widget class="QListWidget" name="TimerTasksListWidget"> <widget class="QListWidget" name="TimerTasksListWidget">
<property name="enabled"> <property name="enabled">
+149
View File
@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 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 enum import Enum
from PySide6.QtCore import (
Qt, QSize, QCoreApplication, QRect, QPoint
)
from PySide6.QtWidgets import (
QAbstractScrollArea, QAbstractItemView,
QTreeWidget, QTreeWidgetItem
)
from PySide6.QtGui import (
QDragEnterEvent, QDragMoveEvent, QDropEvent
)
class TreeItemType(Enum):
GROUP = 0
USER = 1
class ALUserTreeWidget(QTreeWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi()
self.translateUi()
def setupUi(
self
):
__qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(0, u"\u5206\u7ec4/\u7528\u6237");
self.setHeaderItem(__qtreewidgetitem)
self.setObjectName(u"UserTreeWidget")
self.setMinimumSize(QSize(230, 0))
self.setMaximumSize(QSize(250, 16777215))
self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored)
self.setTabKeyNavigation(True)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.setDefaultDropAction(Qt.DropAction.IgnoreAction)
self.setAlternatingRowColors(True)
self.setSortingEnabled(True)
self.setAnimated(True)
self.setAllColumnsShowFocus(False)
self.setHeaderHidden(False)
self.setColumnCount(2)
self.setColumnWidth(0, 150)
self.setColumnWidth(1, 20)
self.header().setCascadingSectionResizes(False)
self.header().setHighlightSections(False)
self.header().setProperty(u"showSortIndicator", True)
def translateUi(
self
):
___qtreewidgetitem = self.headerItem()
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
@staticmethod
def isDragPositionValid(
target_rect: QRect,
drag_pos: QPoint,
) -> bool:
y_offset = drag_pos.y() - target_rect.top()
valid = (y_offset > target_rect.height()*0.2 and
y_offset < target_rect.height()*0.8)
return valid
def dragEnterEvent(
self,
event: QDragEnterEvent
):
super().dragEnterEvent(event)
def dragMoveEvent(
self,
event: QDragMoveEvent
):
super().dragMoveEvent(event)
source_item = self.currentItem()
target_item = self.itemAt(event.position().toPoint())
if source_item is None:
event.ignore()
return
if source_item.type() == TreeItemType.GROUP.value:
if target_item is not None:
event.ignore()
return
elif source_item.type() == TreeItemType.USER.value:
if target_item is None:
event.ignore()
return
if target_item.type() != TreeItemType.GROUP.value:
event.ignore()
return
if target_item.checkState(1) == Qt.CheckState.Unchecked:
event.ignore()
return
if not self.isDragPositionValid(
self.visualItemRect(target_item),
event.position().toPoint()
):
event.ignore()
return
else:
event.ignore()
return
event.acceptProposedAction()
def dropEvent(
self,
event: QDropEvent
):
super().dropEvent(event)
for item_index in range(self.topLevelItemCount()):
self.topLevelItem(item_index).setExpanded(True)
self.setCurrentItem(None)
+48 -32
View File
@@ -32,13 +32,20 @@ class AutoLib(MsgBase):
def __init__( def __init__(
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue output_queue: queue.Queue,
run_config: dict
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
self.__system_config_reader = None self.__run_config = run_config
self.__users_config_reader = None self.__user_config = None
self.__driver = None self.__driver = None
if not self.__initBrowserDriver():
raise Exception("浏览器驱动初始化失败")
else:
if not self.__initDriverUrl():
raise Exception("浏览器驱动URL初始化失败")
self.__initLibOperators()
def __initBrowserDriver( def __initBrowserDriver(
@@ -48,7 +55,11 @@ class AutoLib(MsgBase):
self._showTrace("正在初始化浏览器驱动......") self._showTrace("正在初始化浏览器驱动......")
edge_options = webdriver.EdgeOptions() edge_options = webdriver.EdgeOptions()
if self.__system_config_reader.get("web_driver/headless"): web_driver_config = self.__run_config.get("web_driver", None)
if not web_driver_config:
self._showTrace("未配置浏览器驱动参数 !")
return False
if web_driver_config.get("headless"):
edge_options.add_argument("--headless") edge_options.add_argument("--headless")
edge_options.add_argument("--disable-gpu") edge_options.add_argument("--disable-gpu")
edge_options.add_argument("--no-sandbox") edge_options.add_argument("--no-sandbox")
@@ -76,8 +87,8 @@ class AutoLib(MsgBase):
) )
# init browser driver # init browser driver
self.__driver_path = self.__system_config_reader.get("web_driver/driver_path") self.__driver_path = web_driver_config.get("driver_path")
self.__driver_type = self.__system_config_reader.get("web_driver/driver_type") self.__driver_type = web_driver_config.get("driver_type")
self.__driver_path = os.path.abspath(self.__driver_path) self.__driver_path = os.path.abspath(self.__driver_path)
try: try:
service = None service = None
@@ -149,8 +160,11 @@ class AutoLib(MsgBase):
self, self,
) -> bool: ) -> bool:
url = self.__system_config_reader.get("library/host_url") lib_config = self.__run_config.get("library", None)
url += self.__system_config_reader.get("library/login_url") if not lib_config:
self._showError("未配置图书馆参数 !")
return False
url = lib_config.get("host_url") + lib_config.get("login_url")
self.__driver.get(url) self.__driver.get(url)
if not self.__waitResponseLoad(): if not self.__waitResponseLoad():
return False return False
@@ -161,24 +175,26 @@ class AutoLib(MsgBase):
self, self,
username: str, username: str,
password: str, password: str,
login_config: dict,
run_mode_config: dict,
reserve_info: dict reserve_info: dict
) -> int: ) -> int:
# result : 0 - success, 1 - failed, 2 - passed # result : -1 - terminate, 0 - success, 1 - failed, 2 - passed
result = 2 result = 2
# login # login
if not self.__lib_login.login( if not self.__lib_login.login(
username, username,
password, password,
self.__system_config_reader.get("login/max_attempt", 5), login_config.get("max_attempt", 3),
self.__system_config_reader.get("login/auto_captcha", True), login_config.get("auto_captcha", True),
): ):
return 1 return 1
""" """
Here, we collect the run mode from the config file. Here, we collect the run mode from the run config.
""" """
run_mode = self.__system_config_reader.get("mode/run_mode", 0) run_mode = run_mode_config.get("run_mode", 0)
run_mode = { run_mode = {
"auto_reserve": run_mode&0x1, "auto_reserve": run_mode&0x1,
"auto_checkin": run_mode&0x2, "auto_checkin": run_mode&0x2,
@@ -223,43 +239,43 @@ class AutoLib(MsgBase):
): ):
# if logout is failed, we must make sure the host to be reloaded # if logout is failed, we must make sure the host to be reloaded
# otherwise, the next login may fail # otherwise, the next login may fail
self.__driver.get(self.__system_config_reader.get("library/host_url")) if not self.__initDriverUrl():
return 1 return -1
return result return result
def run( def run(
self, self,
system_config_reader: ConfigReader, user_config: dict
users_config_reader: ConfigReader
): ):
self.__system_config_reader = system_config_reader self.__user_config = user_config
self.__users_config_reader = users_config_reader
if not self.__initBrowserDriver():
return
else:
if not self.__initDriverUrl():
return
self.__initLibOperators()
user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0} user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0}
users = self.__users_config_reader.get("users") users = self.__user_config["users"]
self._showTrace( self._showTrace(f"共发现 {len(users)} 个用户")
f"共发现 {len(users)} 个用户, "\
f"用户配置文件路径: {self.__users_config_reader.configPath()}"
)
for user in users: for user in users:
user_counter["current"] += 1 user_counter["current"] += 1
self._showTrace( self._showTrace(
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......" f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......"
) )
if not user["enabled"]:
self._showTrace(f"用户 {user["username"]} 已跳过")
user_counter["passed"] += 1
continue
r = self.__run( r = self.__run(
username=user["username"], username=user["username"],
password=user["password"], password=user["password"],
login_config=self.__run_config["login"],
run_mode_config=self.__run_config["mode"],
reserve_info=user["reserve_info"], reserve_info=user["reserve_info"],
) )
if r == 0: if r == -1:
self._showTrace(
f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !"
)
break
elif r == 0:
user_counter["success"] += 1 user_counter["success"] += 1
elif r == 1: elif r == 1:
user_counter["failed"] += 1 user_counter["failed"] += 1