1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-17 23:13:03 +08:00

feat(ALConfigWidget): 大更新 - 用户树状列表和其它

1. 在这个 commit 中,我们思考了许久,最终决定将现有的
用户管理列表转为树状列表,以解决用户数量增多时,用户的
选择性管理,分组等问题。
2. 同时因为该更改需要重构很多内容,我们也在该 commit
中决定将所有‘系统配置’更换为‘运行配置’,同时文件名称和
内容变量也相应变为‘run’和‘user’。
3. 重构 AutoLib 和 ALMainWorkers 中的配置相关代码,
以适应新的用户树状列表。

当前迭代更新至 v1.0.0-beta.4, 同时,在该版本的 rc
阶段前,我们计划不再发布 beta 阶段相关的 release
This commit is contained in:
2025-12-13 00:07:33 +08:00
parent 7dcd72939b
commit 55ae4d0d96
6 changed files with 832 additions and 339 deletions
+482 -255
View File
File diff suppressed because it is too large Load Diff
+95 -37
View File
@@ -93,11 +93,26 @@
<string>用户列表</string>
</property>
<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>
<widget class="QListWidget" name="UserListWidget">
<widget class="QTreeWidget" name="UserTreeWidget">
<property name="minimumSize">
<size>
<width>100</width>
<width>230</width>
<height>0</height>
</size>
</property>
@@ -107,18 +122,58 @@
<height>16777215</height>
</size>
</property>
<property name="isWrapping" stdset="0">
<bool>false</bool>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored</enum>
</property>
<property name="viewMode">
<enum>QListView::ViewMode::ListMode</enum>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
<property name="currentRow">
<number>-1</number>
<property name="dragEnabled">
<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 name="sortingEnabled">
<bool>false</bool>
</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>
</item>
<item>
@@ -236,7 +291,7 @@
<widget class="QLabel" name="PasswordLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -325,7 +380,7 @@
<widget class="QLabel" name="UsernameKabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -416,7 +471,7 @@
<widget class="QLabel" name="RoomLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -435,7 +490,7 @@
<widget class="QLabel" name="FloorLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -454,7 +509,7 @@
<widget class="QLabel" name="ExpectRenewDurationLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -473,7 +528,7 @@
<widget class="QLabel" name="EndTimeLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>30</height>
</size>
</property>
@@ -556,7 +611,7 @@
<widget class="QLabel" name="SeatIDLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -575,7 +630,7 @@
<widget class="QLabel" name="ExpectDurationLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -594,7 +649,7 @@
<widget class="QLabel" name="DateLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -738,7 +793,7 @@
</time>
</property>
<property name="displayFormat">
<string>H:mm</string>
<string>HH:mm</string>
</property>
</widget>
</item>
@@ -746,7 +801,7 @@
<widget class="QLabel" name="PlaceLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -869,6 +924,9 @@
<property name="stepType">
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
@@ -902,7 +960,7 @@
<widget class="QLabel" name="MaxBeginTimeDiffLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -921,7 +979,7 @@
<widget class="QLabel" name="MaxEndTimeDiffLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -965,7 +1023,7 @@
</time>
</property>
<property name="displayFormat">
<string>H:mm</string>
<string>HH:mm</string>
</property>
</widget>
</item>
@@ -973,7 +1031,7 @@
<widget class="QLabel" name="BeginTimeLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -1024,7 +1082,7 @@
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
@@ -1047,9 +1105,9 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="SystemConfigWidget">
<widget class="QWidget" name="RunConfigWidget">
<attribute name="title">
<string>系统设置</string>
<string>运行设置</string>
</attribute>
<layout class="QGridLayout" name="SystemConfigWidgetLayout">
<property name="leftMargin">
@@ -1579,7 +1637,7 @@
</property>
<layout class="QGridLayout" name="CurrentConfigLayout">
<item row="1" column="0">
<widget class="QLineEdit" name="CurrentSystemConfigEdit">
<widget class="QLineEdit" name="CurrentRunConfigEdit">
<property name="minimumSize">
<size>
<width>0</width>
@@ -1620,7 +1678,7 @@
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="CurrentSystemConfigLabel">
<widget class="QLabel" name="CurrentRunConfigLabel">
<property name="minimumSize">
<size>
<width>0</width>
@@ -1634,7 +1692,7 @@
</size>
</property>
<property name="text">
<string>当前系统配置路径:</string>
<string>当前运行配置路径:</string>
</property>
</widget>
</item>
@@ -1658,7 +1716,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="BrowseCurrentSystemConfigButton">
<widget class="QPushButton" name="BrowseCurrentRunConfigButton">
<property name="minimumSize">
<size>
<width>35</width>
@@ -1721,7 +1779,7 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="ExportSystemConfigEdit">
<widget class="QLineEdit" name="ExportRunConfigEdit">
<property name="minimumSize">
<size>
<width>0</width>
@@ -1737,7 +1795,7 @@
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="ExportSystemConfigLabel">
<widget class="QLabel" name="ExportRunConfigLabel">
<property name="minimumSize">
<size>
<width>0</width>
@@ -1751,7 +1809,7 @@
</size>
</property>
<property name="text">
<string>系统配置导出路径:</string>
<string>运行配置导出路径:</string>
</property>
</widget>
</item>
@@ -1794,7 +1852,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="BrowseExportSystemConfigButton">
<widget class="QPushButton" name="BrowseExportRunConfigButton">
<property name="minimumSize">
<size>
<width>35</width>
@@ -1836,12 +1894,12 @@
</item>
</layout>
<zorder>ExportUserConfigEdit</zorder>
<zorder>ExportSystemConfigLabel</zorder>
<zorder>ExportRunConfigLabel</zorder>
<zorder>BrowseExportUserConfigButton</zorder>
<zorder>ExportUserConfigLabel</zorder>
<zorder>BrowseExportSystemConfigButton</zorder>
<zorder>BrowseExportRunConfigButton</zorder>
<zorder>ExportConfigButton</zorder>
<zorder>ExportSystemConfigEdit</zorder>
<zorder>ExportRunConfigEdit</zorder>
</widget>
</item>
<item>
+4 -4
View File
@@ -53,9 +53,9 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__config_paths = {
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json")),
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
"timer_tasks": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_tasks.json")),
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
}
self.__alTimerTaskWidget = None
self.__alConfigWidget = None
@@ -81,7 +81,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
# initialize timer task widget, but not show it
self.__alTimerTaskWidget = ALTimerTaskWidget(self, self.__config_paths["timer_tasks"])
self.__alTimerTaskWidget = ALTimerTaskWidget(self, self.__config_paths["timer_task"])
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
+54 -11
View File
@@ -63,6 +63,35 @@ class AutoLibWorker(QThread):
return True
def loadConfigs(
self
) -> bool:
self.showTraceSignal.emit(
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
)
self.__run_config = ConfigReader(
self.__config_paths["run"]
).getConfigs()
self.showTraceSignal.emit(
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.showTraceSignal.emit(
"配置文件加载失败, 请检查配置文件是否正确。"
)
return False
if not self.__user_config.get("groups"):
self.showTraceSignal.emit(
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确"
)
return False
return True
def run(
self
):
@@ -71,21 +100,39 @@ class AutoLibWorker(QThread):
try:
if not self.checkTimeAvailable():
self.showTraceSignal.emit(
"当前时间不在图书馆开放时间内\n"\
"当前时间不在图书馆开放时间内\n"\
" 请在 07:30 - 23:30 之间尝试"
)
return
if not self.checkConfigPaths():
return
self.showTraceSignal.emit("AutoLibrary 开始运行")
if not self.loadConfigs():
return
auto_lib = AutoLib(
self.__input_queue,
self.__output_queue,
self.__run_config
)
auto_lib.run(
ConfigReader(self.__config_paths["system"]),
ConfigReader(self.__config_paths["users"]),
)
if auto_lib is None:
self.showTraceSignal.emit(
"AutoLibrary 初始化失败"
)
return
groups = self.__user_config.get("groups")
for group in groups:
time.sleep(0.2) # wait for the message queue to be empty
if not group["enabled"]:
self.showTraceSignal.emit(
f"任务组 {group["name"]} 已跳过"
)
continue
self.showTraceSignal.emit(
f"正在运行任务组 {group["name"]}"
)
auto_lib.run(
{ "users": group.get("users", []) }
)
except Exception as e:
self.showTraceSignal.emit(
f"AutoLibrary 运行时发生异常 : {e}"
@@ -93,6 +140,7 @@ class AutoLibWorker(QThread):
finally:
if auto_lib:
auto_lib.close()
time.sleep(0.2) # wait for the message queue to be empty
self.showTraceSignal.emit("AutoLibrary 运行结束")
self.finishedSignal.emit()
@@ -109,14 +157,9 @@ class TimerTaskWorker(AutoLibWorker):
config_paths: dict
):
super().__init__(
input_queue,
output_queue,
config_paths,
)
super().__init__(input_queue, output_queue, config_paths)
self.__timer_task = timer_task
self.__stopped = False
def run(
self
+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__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue
output_queue: queue.Queue,
run_config: dict
):
super().__init__(input_queue, output_queue)
self.__system_config_reader = None
self.__users_config_reader = None
self.__run_config = run_config
self.__user_config = None
self.__driver = None
if not self.__initBrowserDriver():
return None
else:
if not self.__initDriverUrl():
return None
self.__initLibOperators()
def __initBrowserDriver(
@@ -48,7 +55,11 @@ class AutoLib(MsgBase):
self._showTrace("正在初始化浏览器驱动......")
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("--disable-gpu")
edge_options.add_argument("--no-sandbox")
@@ -76,8 +87,8 @@ class AutoLib(MsgBase):
)
# init browser driver
self.__driver_path = self.__system_config_reader.get("web_driver/driver_path")
self.__driver_type = self.__system_config_reader.get("web_driver/driver_type")
self.__driver_path = web_driver_config.get("driver_path")
self.__driver_type = web_driver_config.get("driver_type")
self.__driver_path = os.path.abspath(self.__driver_path)
try:
service = None
@@ -149,8 +160,11 @@ class AutoLib(MsgBase):
self,
) -> bool:
url = self.__system_config_reader.get("library/host_url")
url += self.__system_config_reader.get("library/login_url")
lib_config = self.__run_config.get("library", None)
if not lib_config:
self._showError("未配置图书馆参数 !")
return False
url = lib_config.get("host_url") + lib_config.get("login_url")
self.__driver.get(url)
if not self.__waitResponseLoad():
return False
@@ -161,24 +175,26 @@ class AutoLib(MsgBase):
self,
username: str,
password: str,
login_config: dict,
run_mode_config: dict,
reserve_info: dict
) -> int:
# result : 0 - success, 1 - failed, 2 - passed
# result : -1 - terminate, 0 - success, 1 - failed, 2 - passed
result = 2
# login
if not self.__lib_login.login(
username,
password,
self.__system_config_reader.get("login/max_attempt", 5),
self.__system_config_reader.get("login/auto_captcha", True),
login_config.get("max_attempt", 3),
login_config.get("auto_captcha", True),
):
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 = {
"auto_reserve": run_mode&0x1,
"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
# otherwise, the next login may fail
self.__driver.get(self.__system_config_reader.get("library/host_url"))
return 1
if not self.__initDriverUrl():
return -1
return result
def run(
self,
system_config_reader: ConfigReader,
users_config_reader: ConfigReader
user_config: dict
):
self.__system_config_reader = system_config_reader
self.__users_config_reader = users_config_reader
if not self.__initBrowserDriver():
return
else:
if not self.__initDriverUrl():
return
self.__initLibOperators()
self.__user_config = user_config
user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0}
users = self.__users_config_reader.get("users")
self._showTrace(
f"共发现 {len(users)} 个用户, "\
f"用户配置文件路径: {self.__users_config_reader.configPath()}"
)
users = self.__user_config["users"]
self._showTrace(f"共发现 {len(users)} 个用户")
for user in users:
user_counter["current"] += 1
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(
username=user["username"],
password=user["password"],
login_config=self.__run_config["login"],
run_mode_config=self.__run_config["mode"],
reserve_info=user["reserve_info"],
)
if r == 0:
if r == -1:
self._showTrace(
f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !"
)
break
elif r == 0:
user_counter["success"] += 1
elif r == 1:
user_counter["failed"] += 1