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

Compare commits

...

39 Commits

Author SHA1 Message Date
KenanZhu 149910d628 chore(release): v1.0.0 2025-12-22 15:24:31 +08:00
KenanZhu 2a7ed099bf docs(readme.md): 更新 readme 文档 2025-12-22 15:23:56 +08:00
KenanZhu 473f32ca29 chore(batchs): 新增界面资源和应用资源的编译脚本 2025-12-22 15:23:47 +08:00
KenanZhu 580052f1e3 chore(icons): 添加多种图标格式,将当前的图标尺寸从 1024x1024 调整为 32x32 2025-12-22 11:55:33 +08:00
KenanZhu 6abf530307 optimize(ALTimerTaskWidget, ALConfigWidget): 优化定时器和用户设置的任务列表排序 2025-12-22 11:53:45 +08:00
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
KenanZhu 1b172ad396 style(*): some small style changes 2025-11-30 18:46:12 +08:00
KenanZhu 05c9d433f4 hotfix(LibRenew): fix the serious bug that the renew process always failed
in this hotfix, we fix the renew bug because we
do not refresh the page after renew the seat,
this will make the subsequent operators, such as
logout ... unable to in progress.
2025-11-30 18:42:20 +08:00
KenanZhu 65ca40438d fix(ALMainWindow.ui): fix the convert error of ALMainWindow.ui
we fix the convert error of ALMainWindow.ui by
using pyside6-uic to convert the ui file to python code
2025-11-29 22:31:18 +08:00
KenanZhu 0a8763add5 feat(gui): breaking changes - Timer Task Management
1. we add menu actions 'manual' and 'about', so
you can click actions to open manual and about dialog.
2. we introduce timer task management feature, so
you can add, delete timer tasks to auto run task.
3. other style improvement in gui...
2025-11-29 20:03:45 +08:00
KenanZhu c5e589f3d1 fix(ALConfigWidget): optimize the logic when delete user list item 2025-11-29 19:52:22 +08:00
KenanZhu 5e5deba773 fix(LibReserve): fix the mistakely passed parameter 'reserve_info'
we forget to pass the username because the
'reserve_info' do not contain the username
2025-11-28 15:15:39 +08:00
KenanZhu 842fb434f4 feat(AutoLib): new feature 'Auto Renew' 2025-11-28 15:03:51 +08:00
KenanZhu 6cabddf0cd fix(operators): optimized the reserve information pre-check and more readable output 2025-11-28 15:00:09 +08:00
KenanZhu 0322558339 fix(operators): the operations's result message only show in their output queue 2025-11-28 14:58:13 +08:00
KenanZhu 703ee527ae fix(LibChecker): fix the checker of check in and renew
we only check the reservations and their status in
today's record, and return the checked renewable
record for the upcoming new feature 'Auto-Renew'
2025-11-28 14:54:37 +08:00
KenanZhu 9a925fecb6 fix(operators): fix some type hint, and add imports for LibRenew 2025-11-28 14:53:08 +08:00
KenanZhu 189fddfb6a fix(LibReserve): more fast operations of reserve 2025-11-28 14:46:17 +08:00
KenanZhu c2d53a8b78 chore(*): refactor the project structure 2025-11-25 08:48:18 +08:00
KenanZhu b99431476a hotfix(LibChecker): optimize the reserve records check process 2025-11-22 15:12:40 +08:00
KenanZhu 977c0835b7 hotfix(ALMainWindow): fix the config file paths initialization 2025-11-22 15:11:25 +08:00
KenanZhu cd565ec57d feat(gui.SeatMapWidget): add seat select map widget 2025-11-22 14:29:01 +08:00
KenanZhu 9f17474c1b fix(gui): optimize the config files' status management 2025-11-22 14:27:40 +08:00
KenanZhu 04d66346dc fix(ALConfigWidget): optimize the config window usage
add date calendar popup so that user can select
the date more easily
fix some file dialog title display issue
default max diff time change to 30 minutes
2025-11-22 14:23:35 +08:00
KenanZhu f858295af1 refactor(LibChecker): refactor the code of LibChecker to make it more readable and maintainable 2025-11-22 14:16:38 +08:00
KenanZhu cd6c899388 fix(*): optimize the operators' performance when invoking webdriver
we consume the wait time of webdriver and its
implicit wait time
2025-11-22 14:13:23 +08:00
49 changed files with 5742 additions and 1819 deletions
+10 -6
View File
@@ -8,10 +8,14 @@ build/
dist/
model/*.onnx
driver/*.exe
gui/configs/*.json
gui/translators/qtbase_zh_CN.qm
gui/AutoLibraryResources.py
gui/AutoLibraryResource.py
gui/Ui_ALMainWindow.py
gui/Ui_ALConfigWidget.py
src/gui/configs/*.json
src/gui/translators/qtbase_zh_CN.qm
src/gui/AutoLibraryResources.py
src/gui/AutoLibraryResource.py
src/gui/Ui_ALMainWindow.py
src/gui/Ui_ALConfigWidget.py
src/gui/Ui_ALTimerTaskWidget.py
src/gui/Ui_ALAddTimerTaskDialog.py
src/gui/Ui_ALAboutDialog.py
Main.spec
-34
View File
@@ -1,34 +0,0 @@
# -*- 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 queue
from LibOperator import LibOperator
class LibRenew(LibOperator):
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
):
super().__init__(input_queue, output_queue)
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
pass
-800
View File
@@ -1,800 +0,0 @@
# -*- 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 sys
from PySide6.QtCore import (
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
)
from PySide6.QtWidgets import (
QWidget, QLineEdit, QMessageBox, QFileDialog, QListWidgetItem
)
from PySide6.QtGui import QCloseEvent
from .Ui_ALConfigWidget import Ui_ALConfigWidget
from ConfigReader import ConfigReader
from ConfigWriter import ConfigWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
configWidgetCloseSingal = Signal(dict)
def __init__(
self,
parent = None,
config_paths = {
"system":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
"users":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
}
):
super().__init__(parent)
self.setupUi(self)
self.connectSignals()
self.modifyUi()
self.__config_paths = config_paths
self.__system_config_data = self.loadSystemConfig(self.__config_paths["system"])
self.__users_config_data = self.loadUsersConfig(self.__config_paths["users"])
if not self.__system_config_data:
self.initlizeDefaultConfig("system")
if not self.__users_config_data:
self.initlizeDefaultConfig("users")
self.initlizeConfigToWidget("system", self.__system_config_data)
self.initlizeConfigToWidget("users", self.__users_config_data)
def modifyUi(
self
):
self.initlizeFloorRoomMap()
self.initilizeUserInfoWidget()
def connectSignals(
self
):
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
self.BrowseCurrentSystemConfigButton.clicked.connect(self.onBrowseCurrentSystemConfigButtonClicked)
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
self.BrowseExportSystemConfigButton.clicked.connect(self.onBrowseExportSystemConfigButtonClicked)
self.BrowseExportUserConfigButton.clicked.connect(self.onBrowseExportUserConfigButtonClicked)
self.ExportConfigButton.clicked.connect(self.onExportConfigButtonClicked)
self.NewConfigButton.clicked.connect(self.onNewConfigButtonClicked)
self.LoadConfigButton.clicked.connect(self.onLoadConfigButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def closeEvent(
self,
event: QCloseEvent
):
self.configWidgetCloseSingal.emit(self.__config_paths)
super().closeEvent(event)
def initlizeFloorRoomMap(
self
):
self.__floor_map = {
"2": "二层",
"3": "三层",
"4": "四层",
"5": "五层"
}
self.__room_map = {
"1": "二层内环",
"2": "二层外环",
"3": "三层内环",
"4": "三层外环",
"5": "四层内环",
"6": "四层外环",
"7": "四层期刊区",
"8": "五层考研"
}
self.__floor_rmap = {
v: k for k, v in self.__floor_map.items()
}
self.__room_rmap = {
v: k for k, v in self.__room_map.items()
}
self.__floor_room_map = {
"二层": ["二层内环", "二层外环"],
"三层": ["三层内环", "三层外环"],
"四层": ["四层内环", "四层外环", "四层期刊区"],
"五层": ["五层考研"]
}
def initlizeDefaultConfigPaths(
self
) -> dict:
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
return {
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json"))
}
def initlizeDefaultConfig(
self,
which: str
):
default_config_paths = self.initlizeDefaultConfigPaths()
if which == "system":
self.__system_config_data = self.defaultSystemConfig()
self.__config_paths["system"] = default_config_paths["system"]
self.saveSystemConfig(self.__config_paths["system"], self.__system_config_data)
elif which == "users":
self.__users_config_data = self.defaultUsersConfig()
self.__config_paths["users"] = default_config_paths["users"]
self.saveUsersConfig(self.__config_paths["users"], self.__users_config_data)
if which == "system":
file_type = "系统配置文件"
elif which == "users":
file_type = "用户配置文件"
QMessageBox.information(
self,
"提示 - AutoLibrary",
f"{file_type}已初始化, \n"\
f" 文件路径: {self.__config_paths[which]}"
)
def initlizeConfigToWidget(
self,
which: str,
config_data: dict
):
if which == "system":
self.setSystemConfigToWidget(config_data)
self.CurrentSystemConfigEdit.setText(self.__config_paths["system"])
elif which == "users":
self.initilizeUserInfoWidget()
self.fillUsersList(config_data)
self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
def defaultSystemConfig(
self
) -> dict:
return {
"library": {
"host_url": "http://10.1.20.7",
"login_url": "/login"
},
"login": {
"auto_captcha": True,
"max_attempt": 3
},
"web_driver": {
"driver_type": "edge",
"driver_path": "msedgedriver.exe",
"headless": False
},
"mode": {
"run_mode": 1
}
}
def defaultUsersConfig(
self
) -> dict:
return {
"users": []
}
def collectSystemConfigFromWidget(
self
) -> dict:
system_config = self.defaultSystemConfig()
# library config is never changed
system_config["login"]["auto_captcha"] = self.AutoCaptchaCheckBox.isChecked()
system_config["login"]["max_attempt"] = self.LoginAttemptSpinBox.value()
system_config["web_driver"]["driver_type"] = self.BrowserTypeComboBox.currentText()
system_config["web_driver"]["driver_path"] = self.BrowseBrowserDriverEdit.text()
system_config["web_driver"]["headless"] = self.HeadlessCheckBox.isChecked()
run_mode = 0
if self.AutoReserveCheckBox.isChecked():
run_mode |= 0x01
if self.AutoCheckinCheckBox.isChecked():
run_mode |= 0x02
if self.AutoRenewalCheckBox.isChecked():
run_mode |= 0x04
system_config["mode"]["run_mode"] = run_mode
return system_config
def setSystemConfigToWidget(
self,
system_config: dict
):
self.HostUrlEdit.setText(system_config["library"]["host_url"])
self.LoginUrlEdit.setText(system_config["library"]["login_url"])
self.AutoCaptchaCheckBox.setChecked(system_config["login"]["auto_captcha"])
self.LoginAttemptSpinBox.setValue(system_config["login"]["max_attempt"])
self.BrowserTypeComboBox.setCurrentText(system_config["web_driver"]["driver_type"])
driver_path = os.path.abspath(system_config["web_driver"]["driver_path"])
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
self.HeadlessCheckBox.setChecked(system_config["web_driver"]["headless"])
run_mode = system_config["mode"]["run_mode"]
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
def initilizeUserInfoWidget(
self
):
self.UsernameEdit.setText("")
self.PasswordEdit.setText("")
self.UserListWidget.setSortingEnabled(True)
self.PasswordEdit.setEchoMode(QLineEdit.Password)
self.ShowPasswordCheckBox.setChecked(False)
self.FloorComboBox.setCurrentIndex(1) # use for the '__init__' to effect the signal
self.FloorComboBox.setCurrentIndex(0)
self.DateEdit.setDate(QDate.currentDate())
self.DateEdit.setMinimumDate(QDate.currentDate())
self.DateEdit.setMaximumDate(QDate.currentDate())
if QTime.currentTime() > QTime(18, 0, 0) and QTime.currentTime() < QTime(23, 0, 0):
self.DateEdit.setMaximumDate(QDate.currentDate().addDays(1))
self.BeginTimeEdit.setTime(QTime.currentTime())
self.PreferEarlyBeginTimeCheckBox.setChecked(False)
self.MaxBeginTimeDiffSpinBox.setValue(10)
self.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
self.PreferLateEndTimeCheckBox.setChecked(False)
self.MaxEndTimeDiffSpinBox.setValue(10)
self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
self.SatisfyDurationCheckBox.setChecked(False)
def collectUserConfigFromUserInfoWidget(
self
) -> dict:
user_config = {
"username": self.UsernameEdit.text(),
"password": self.PasswordEdit.text(),
"reserve_info": {
"begin_time":{},
"end_time": {}
}
}
user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd")
user_config["reserve_info"]["place"] = self.PlaceComboBox.currentText()
user_config["reserve_info"]["floor"] = self.__floor_rmap[self.FloorComboBox.currentText()]
user_config["reserve_info"]["room"] = self.__room_rmap[self.RoomComboBox.currentText()]
user_config["reserve_info"]["seat_id"] = self.SeatIDEdit.text()
user_config["reserve_info"]["begin_time"]["time"] = self.BeginTimeEdit.time().toString("HH:mm")
user_config["reserve_info"]["begin_time"]["max_diff"] = self.MaxBeginTimeDiffSpinBox.value()
user_config["reserve_info"]["begin_time"]["prefer_early"] = self.PreferEarlyBeginTimeCheckBox.isChecked()
user_config["reserve_info"]["end_time"]["time"] = self.EndTimeEdit.time().toString("HH:mm")
user_config["reserve_info"]["end_time"]["max_diff"] = self.MaxEndTimeDiffSpinBox.value()
user_config["reserve_info"]["end_time"]["prefer_early"] = not self.PreferLateEndTimeCheckBox.isChecked()
user_config["reserve_info"]["expect_duration"] = self.ExpectDurationSpinBox.value()
user_config["reserve_info"]["satisfy_duration"] = self.SatisfyDurationCheckBox.isChecked()
return user_config
def collectUserConfigFromUserListWidget(
self,
index: int
) -> dict:
user_config = self.defaultUsersConfig()
if index < 0 or index >= self.UserListWidget.count():
return user_config
user_item = self.UserListWidget.item(index)
if user_item:
user_config = user_item.data(Qt.UserRole)
return user_config
def setUserConfigToWidget(
self,
user_config: dict
) -> None:
try:
self.UsernameEdit.setText(user_config["username"])
self.PasswordEdit.setText(user_config["password"])
self.DateEdit.setDate(QDate.fromString(user_config["reserve_info"]["date"], "yyyy-MM-dd"))
self.PlaceComboBox.setCurrentText(user_config["reserve_info"]["place"])
self.FloorComboBox.setCurrentText(self.__floor_map[user_config["reserve_info"]["floor"]])
self.RoomComboBox.setCurrentText(self.__room_map[user_config["reserve_info"]["room"]])
self.SeatIDEdit.setText(user_config["reserve_info"]["seat_id"])
self.BeginTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["begin_time"]["time"], "H:mm"))
self.MaxBeginTimeDiffSpinBox.setValue(user_config["reserve_info"]["begin_time"]["max_diff"])
self.PreferEarlyBeginTimeCheckBox.setChecked(user_config["reserve_info"]["begin_time"]["prefer_early"])
self.EndTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["end_time"]["time"], "H:mm"))
self.MaxEndTimeDiffSpinBox.setValue(user_config["reserve_info"]["end_time"]["max_diff"])
self.PreferLateEndTimeCheckBox.setChecked(not user_config["reserve_info"]["end_time"]["prefer_early"])
self.ExpectDurationSpinBox.setValue(user_config["reserve_info"]["expect_duration"])
self.SatisfyDurationCheckBox.setChecked(user_config["reserve_info"]["satisfy_duration"])
except:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"用户配置文件读取发生错误 !\n"\
f"用户: {user_config['username']} 配置文件可能已损坏"
)
def loadSystemConfig(
self,
system_config_path: str
) -> dict:
try:
if not system_config_path or not os.path.exists(system_config_path):
raise Exception("文件路径不存在")
system_config = ConfigReader(system_config_path).getConfigs()
if system_config and "library" in system_config\
and "web_driver" in system_config\
and "login" in system_config:
return system_config
return None
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"系统配置文件读取发生错误 ! : {e}\n"\
f"文件路径: {system_config_path}"
)
return None
def saveSystemConfig(
self,
system_config_path: str,
system_config_data: dict
) -> bool:
try:
if not system_config_path:
raise Exception("文件路径为空")
if not system_config_data or not isinstance(system_config_data, dict):
raise Exception("系统配置数据为空或类型错误")
ConfigWriter(system_config_path, system_config_data)
return True
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"配置文件写入发生错误 ! : {e}\n"\
f"文件路径: {system_config_path}"
)
return False
def loadUsersConfig(
self,
users_config_path: str
) -> dict:
try:
if not users_config_path or not os.path.exists(users_config_path):
raise Exception("文件路径不存在")
users_config = ConfigReader(users_config_path).getConfigs()
if users_config and "users" in users_config:
return users_config
return None
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取发生错误 ! : {e}\n"\
f"文件路径: {users_config_path}"
)
return None
def saveUsersConfig(
self,
users_config_path: str,
users_config_data: dict
) -> bool:
try:
if not users_config_path:
raise Exception("文件路径为空")
if not users_config_data or not isinstance(users_config_data, dict):
raise Exception("用户配置数据为空或类型错误")
ConfigWriter(users_config_path, users_config_data)
return True
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件写入发生错误 ! : {e}\n"\
f"文件路径: \n{users_config_path}"
)
return False
def saveConfigs(
self,
system_config_path: str,
users_config_path: str
) -> bool:
if users_config_path:
self.__users_config_data = self.defaultUsersConfig()
for index in range(self.UserListWidget.count()):
user_config = self.collectUserConfigFromUserListWidget(index)
if user_config:
self.__users_config_data["users"].append(user_config)
if not self.saveUsersConfig(
users_config_path,
self.__users_config_data
):
return False
if system_config_path:
self.__system_config_data = self.collectSystemConfigFromWidget()
if not self.saveSystemConfig(
system_config_path,
self.__system_config_data
):
return False
return True
def loadConfig(
self,
config_path: str
) -> bool:
if not config_path:
config_path = QFileDialog.getOpenFileName(
self,
"从现有配置文件中加载 - AutoLibrary",
f"{QDir.toNativeSeparators(QDir.currentPath())}",
"JSON 文件 (*.json);;所有文件 (*)"
)[0]
if not config_path:
return False
try:
system_config = self.loadSystemConfig(config_path)
users_config = self.loadUsersConfig(config_path)
if system_config is not None:
self.__system_config_data.update(system_config)
self.setSystemConfigToWidget(self.__system_config_data)
return True
if users_config is not None:
self.__users_config_data.update(users_config)
self.fillUsersList(self.__users_config_data)
return True
except:
return False
def fillUsersList(
self,
users_config_data: list[dict]
):
self.UserListWidget.clear()
if "users" in users_config_data:
for user in users_config_data["users"]:
user_item = QListWidgetItem(user["username"])
user_item.setData(Qt.UserRole, user)
self.UserListWidget.addItem(user_item)
def addUser(
self
):
new_user = {
"username": f"新用户-{self.UserListWidget.count()}",
"password": "000000",
"reserve_info": {
"date": f"{QDate.currentDate().toString("yyyy-MM-dd")}",
"place": "\u56fe\u4e66\u9986",
"floor": "2",
"room": "1",
"seat_id": "",
"begin_time": {
"time": f"{QTime.currentTime().toString("hh:mm")}",
"max_diff": 0,
"prefer_early": False
},
"end_time": {
"time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
"max_diff": 0,
"prefer_early": True
},
"expect_duration": 2.0,
"satisfy_duration": False
}
}
user_item = QListWidgetItem(new_user["username"])
user_item.setData(Qt.UserRole, new_user)
self.UserListWidget.addItem(user_item)
self.UserListWidget.setCurrentItem(user_item)
self.setUserConfigToWidget(new_user)
def delUser(
self
):
current_item = self.UserListWidget.currentItem()
if current_item:
self.UserListWidget.takeItem(self.UserListWidget.row(current_item))
self.UserListWidget.setCurrentItem(None)
@Slot()
def onShowPasswordCheckBoxChecked(
self,
checked: bool
):
if checked:
self.PasswordEdit.setEchoMode(QLineEdit.Normal)
else:
self.PasswordEdit.setEchoMode(QLineEdit.Password)
@Slot()
def onFloorComboBoxCurrentIndexChanged(
self
):
floor = self.FloorComboBox.currentText()
self.RoomComboBox.clear()
self.RoomComboBox.addItems(self.__floor_room_map[floor])
self.RoomComboBox.setCurrentIndex(0)
@Slot()
def onUserListWidgetCurrentItemChanged(
self,
current: QListWidgetItem,
previous: QListWidgetItem
):
# dont care about the 'self.__users_config_data', we already
# cant effectively update the data of each user, due to the
# possiblity of frequency edit. we just let the QListWidget
# help us.
if not current:
self.initilizeUserInfoWidget()
return
if previous:
user = self.collectUserConfigFromUserInfoWidget()
if user:
previous.setText(user["username"])
previous.setData(Qt.UserRole, user)
user = current.data(Qt.UserRole)
if user:
self.setUserConfigToWidget(user)
@Slot()
def onAddUserButtonClicked(
self
):
self.addUser()
@Slot()
def onDelUserButtonClicked(
self
):
self.delUser()
@Slot()
def onBrowseBrowserDriverButtonClicked(
self
):
browser_driver_path = QFileDialog.getOpenFileName(
self,
"选择浏览器驱动 - AutoLibrary",
self.CurrentSystemConfigEdit.text(),
"可执行文件 (*.exe);;所有文件 (*)"
)[0]
if browser_driver_path:
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
@Slot()
def onBrowseCurrentSystemConfigButtonClicked(
self
):
system_config_path = QFileDialog.getOpenFileName(
self,
"选择其它的系统配置 - AutoLibrary",
self.CurrentSystemConfigEdit.text(),
"JSON 文件 (*.json);;所有文件 (*)"
)[0]
if system_config_path:
system_config_path = QDir.toNativeSeparators(system_config_path)
if self.loadConfig(system_config_path):
self.__config_paths["system"] = system_config_path
self.CurrentSystemConfigEdit.setText(system_config_path)
@Slot()
def onBrowseCurrentUserConfigButtonClicked(
self
):
users_config_path = QFileDialog.getOpenFileName(
self,
"选择其它的用户配置 - AutoLibrary",
self.CurrentUserConfigEdit.text(),
"JSON 文件 (*.json);;所有文件 (*)"
)[0]
if users_config_path:
users_config_path = QDir.toNativeSeparators(users_config_path)
if self.loadConfig(users_config_path):
self.__config_paths["users"] = users_config_path
self.CurrentUserConfigEdit.setText(users_config_path)
@Slot()
def onBrowseExportSystemConfigButtonClicked(
self
):
system_config_path = QFileDialog.getSaveFileName(
self,
"导出系统配置 - AutoLibrary",
self.CurrentSystemConfigEdit.text(),
"JSON 文件 (*.json);;所有文件 (*)"
)[0]
if system_config_path:
self.ExportSystemConfigEdit.setText(QDir.toNativeSeparators(system_config_path))
@Slot()
def onBrowseExportUserConfigButtonClicked(
self
):
users_config_path = QFileDialog.getSaveFileName(
self,
"导出用户配置 - AutoLibrary",
self.CurrentUserConfigEdit.text(),
"JSON 文件 (*.json);;所有文件 (*)"
)[0]
if users_config_path:
self.ExportUserConfigEdit.setText(QDir.toNativeSeparators(users_config_path))
@Slot()
def onExportConfigButtonClicked(
self
):
msg = ""
system_config_path = self.ExportSystemConfigEdit.text()
users_config_path = self.ExportUserConfigEdit.text()
if system_config_path:
if self.saveConfigs(
system_config_path,
users_config_path=""
):
msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
if users_config_path:
if self.saveConfigs(
"", users_config_path
):
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
if msg:
QMessageBox.information(
self,
"信息 - AutoLibrary",
msg
)
@Slot()
def onLoadConfigButtonClicked(
self
):
self.loadConfig("")
@Slot()
def onNewConfigButtonClicked(
self
):
file_path = self.CurrentSystemConfigEdit.text()
folder_dir = QFileDialog.getExistingDirectory(
self,
"选择新建配置的文件夹 - AutoLibrary",
QDir.toNativeSeparators(QFileInfo(os.path.abspath(file_path)).absoluteDir().path())
)
if not folder_dir:
return
system_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "system.json"))
users_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "users.json"))
system_exists = os.path.isfile(system_config_path)
users_exists = os.path.isfile(users_config_path)
if system_exists or users_exists:
exist_files = []
if system_exists:
exist_files.append(system_config_path)
if users_exists:
exist_files.append(users_config_path)
reply = QMessageBox.information(
self,
"信息 - AutoLibrary",
f"文件夹中已存在以下文件, 是否覆盖 ?\n{chr(10).join(exist_files)}",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
return
self.__system_config_data = self.defaultSystemConfig()
self.__users_config_data = self.defaultUsersConfig()
self.__config_paths = {
"system": system_config_path,
"users": users_config_path
}
self.initlizeConfigToWidget("system", self.__system_config_data)
self.initlizeConfigToWidget("users", self.__users_config_data)
@Slot()
def onConfirmButtonClicked(
self
):
if self.UserListWidget.currentItem() is not None:
user = self.collectUserConfigFromUserInfoWidget()
if user:
self.UserListWidget.currentItem().setData(Qt.UserRole, user)
if self.saveConfigs(
self.__config_paths["system"],
self.__config_paths["users"]
):
QMessageBox.information(
self,
"信息 - AutoLibrary",
"配置文件保存成功 !\n"
f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
f"用户配置文件路径: \n{self.__config_paths['users']}"
)
else:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"配置文件保存失败, 请检查文件路径权限"
)
self.close()
@Slot()
def onCancelButtonClicked(
self
):
self.close()
-317
View File
@@ -1,317 +0,0 @@
# -*- 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 sys
import time
import queue
from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
)
from PySide6.QtWidgets import (
QMainWindow, QMenu
)
from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon
)
from .Ui_ALMainWindow import Ui_ALMainWindow
from .ALConfigWidget import ALConfigWidget
from . import AutoLibraryResource
from AutoLib import AutoLib
from ConfigReader import ConfigReader
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
self.__stopped = False
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()
def stop(
self
):
self.__stopped = True
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
def __init__(
self
):
super().__init__()
self.__class_name = self.__class__.__name__
self.setupUi(self)
self.__input_queue = queue.Queue()
self.__output_queue = queue.Queue()
self.__config_paths = {
"system":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
"users":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
}
self.__alConfigWidget = None
self.__auto_lib_thread = None
self.modifyUi()
self.connectSignals()
self.startMsgPolling()
def modifyUi(
self
):
icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
self.setWindowIcon(icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
def connectSignals(
self
):
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
self.StartButton.clicked.connect(self.onStartButtonClicked)
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
def closeEvent(
self,
event: QCloseEvent
):
if self.__timer and self.__timer.isActive():
self.__timer.stop()
if self.__alConfigWidget:
self.__alConfigWidget.close()
super().closeEvent(event)
def appendToTextEdit(
self,
text: str
):
cursor = self.MessageIOTextEdit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(text + "\n")
self.MessageIOTextEdit.setTextCursor(cursor)
self.MessageIOTextEdit.ensureCursorVisible()
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def startMsgPolling(
self
):
self.__timer = QTimer()
self.__timer.timeout.connect(self.pollMsgQueue)
self.__timer.start(100)
def setControlButtons(
self,
config_button_enabled: bool,
start_button_enabled: bool,
stop_button_enabled: bool
):
self.ConfigButton.setEnabled(config_button_enabled)
self.StartButton.setEnabled(start_button_enabled)
self.StopButton.setEnabled(stop_button_enabled)
@Slot()
def showMsg(
self,
msg: str
):
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
@Slot()
def showTrace(
self,
msg: str
):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
@Slot()
def pollMsgQueue(
self
):
try:
while True:
msg = self.__output_queue.get_nowait()
self.appendToTextEdit(msg)
except queue.Empty:
pass
@Slot(dict)
def onConfigWidgetClosed(
self,
config_paths: dict
):
if self.__alConfigWidget:
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
self.ConfigButton.setEnabled(True)
self.StartButton.setEnabled(True)
self.StopButton.setEnabled(False)
self.__config_paths = config_paths
@Slot()
def onConfigButtonClicked(
self
):
if self.__alConfigWidget is None:
self.__alConfigWidget = ALConfigWidget(
self,
self.__config_paths
)
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.setWindowFlags(Qt.Window)
self.__alConfigWidget.setWindowModality(Qt.ApplicationModal)
self.__alConfigWidget.show()
self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow()
self.ConfigButton.setEnabled(False)
@Slot()
def onStartButtonClicked(
self
):
self.setControlButtons(False, False, True)
if self.__auto_lib_thread is None:
self.__auto_lib_thread = AutoLibWorker(
self.__input_queue,
self.__output_queue,
self.__config_paths,
)
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
self.__auto_lib_thread.start()
@Slot()
def onStopButtonClicked(
self
):
if self.__auto_lib_thread:
self.showTrace("正在停止操作......")
self.__auto_lib_thread.stop()
self.__auto_lib_thread.wait()
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.deleteLater()
self.__auto_lib_thread = None
self.setControlButtons(True, True, False)
@Slot()
def onSendButtonClicked(
self
):
msg = self.MessageEdit.text().strip()
if not msg:
return
self.showMsg(msg)
self.MessageEdit.clear()
+108 -2
View File
@@ -1,6 +1,112 @@
# AutoLibrary
---
请访问[AutoLibrary 网站](http://autolibrary.cv)
![AutoLibrary Logo](./src/gui/icons/AutoLibrary_128x128.ico)
Please access the [AutoLibrary Website](http://autolibrary.cv)
![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary)
![Issue](https://img.shields.io/github/issues/KenanZhu/AutoLibrary)
![Release](https://img.shields.io/github/v/release/KenanZhu/AutoLibrary)
![Download](https://img.shields.io/github/downloads/KenanZhu/AutoLibrary/total)
了解更多请访问 [_AutoLibrary 网站_](http://autolibrary.cv)
---
### 功能
1. 自动预约 - 支持自动预约
2. 自动续约 - 支持自动续约
3. 自动签到 - 支持自动签到
4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组
5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行
_1,2,3 的具体操作方法和注意事项请访问网站 [帮助手册](https://autolibrary.cv/docs/manual_lists.html)_
### 特点
#### 关于预约等操作的注意事项
工具会自动处理登录过程的验证码识别过程,正常情况下单次识别准确率在 90% 以上,如遇验证码识别错误,大概率是校园网网络环境不佳导致的。
只要确保处于校园网网络环境内,工具都是可以正常运行的。操作处理速度基本上取决于校园网的网络环境,一般情况下在 3-4 秒左右即可完成一个用户的操作,完全满足正常使用需求。
> [!NOTE]
> 工具仅作为正常的预约,签到和续约的图书馆辅助工具,请勿干扰图书馆的正常运行(如故意预约多个座位,或同时预约大量的用户等,对此影响图书馆正常运行本工具概不负责,请在善用工具方便自己的情况下尽量不用影响其他同学的使用)。
#### 关于批量操作的注意事项
批量操作时,建议将需要操作的用户分成多个组,每个组的用户数量不要超过 4 人(即一整张桌子的数量),否则会影响操作效率,大量用户同时预约会一定程度上增加图书馆服务器的压力,影响正常使用。根据需要在用户管理界面中可以勾选本次操作是否跳过该用户,以提高运行效率。
#### 关于定时任务的注意事项
定时任务会在指定的时间自动运行,运行时会根据当前预约信息进行操作。一般情况下不建议设置两个运行开始时间比较接近的定时任务,否则后一个任务会等待前一个任务完成后才会运行,按照队列的顺序执行。
### 如何使用
1. 下载最新版本的 AutoLibrary 工具压缩包,[点击这里](https://github.com/KenanZhu/AutoLibrary/releases)。
2. 解压下载的文件到任意目录。
3. 运行 `AutoLibrary.exe` 文件。
4. 按照提示操作即可。
#### 平台支持 & 编译步骤
本工具目前仅支持 Windows 平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和 macOS 上运行,这里提供简单的编译步骤:
1. 确保系统安装了 Python 3.13 或以上版本。
2. 安装 pyside6 selenium ddddocr 库,命令为 `pip install pyside6 selenium ddddocr`
3.`src/gui/batchs` 目录下运行 `complie_ui.bat` linux 和 macOS 系统使用 `complie_ui.sh`) 文件来编译 Qt 的 UI 文件。
4. 在上一步相同目录内运行 `complie_rc.bat` linux 和 macOS 系统使用 `complie_rc.sh` 文件来编译 Qt 的资源文件。
5. 待上述步骤完成后,运行 `src/Main.py` 文件即可。
*注意 1*:如果 python 使用的是虚拟环境,请在虚拟环境安装依赖后,在激活的虚拟环境终端中使用 `cd src/gui/batchs` 命令切换到 `batchs` 目录下,再运行编译脚本。否则会提示缺少必要的 Qt PySide 依赖库。
*注意 2*:由于 ddddocr 的代码版本问题,其中的 `__init__.py` 文件中的函数 `def classification(self, img: bytes):` 中的 `image.resize` 方法传入了不符合当前版本的`resample` 参数,导致在 Python 3.13 以上中运行时会报错。请将 `image.resize` 方法中的参数替换为 `resample=Image.Resampling.LANCZOS`,具体函数如下:
```python
def classification(self, img: bytes):
image = Image.open(io.BytesIO(img))
image = image.resize((int(image.size[0]*(64/image.size[1])), 64), Image.Resampling.LANCZOS).convert('L')
^^^^^
请将上述参数替换为如上所示的 `Image.Resampling.LANCZOS`
...
```
### Q&A
#### 为什么开发这个工具?
其实是因为自己在使用图书馆时,发现预约和续约等操作比较麻烦,图书馆的登录系统也比较老旧,验证码错误后需要重复输入。有时候网络环境不好的时候,登录很慢,想赶紧签到或者预约明天的座位,而且会有朋友让我帮忙预约一个座位,这时候就需要一个工具来帮助自己快速完成这些操作,自己则不需要管这些事情,只需要专注自己的事情就可以了。
#### 工具后续会收费吗?
不会,本工具完全免费使用,也不会有任何收费项,如果你觉工具对你有帮助,可以为我捐助一瓶可乐的价格,以用于 AutoLibrary 网站的维护和软件的稳定更新。
#### 会有手机端的版本吗?
暂时没有考虑,而且也没有足够的能力和精力开发多平台的版本并维护,所以暂时只提供 Windows 版本。
#### 后续会有哪些功能?
由于本人的时间和精力有限,开发后续功能会有所取舍,如果你有什么功能建议或者想参与开发,欢迎联系我。
首先当前 1.0.0 版本的功能对于正常使用已经足够,不过会考虑后续着重完善 2-4 预约时的操作流程,暂时由以下构想:
1. 2-4 人一起预约时,往往会偏向于预约并排或对面的整个空座位,这时候工具会按照一定策略查询搜索符合条件的座位,并预约并排或对面的整个座位,而不是各自独立预约。
2. 预约时会考虑到组内用户的预约时间是否冲突,若冲突则会提示用户是否继续预约,若用户选择继续预约,则会按需要调整预约时间,再进行预约。
3. 对于比较固定的用户,会考虑在定时任务管理中添加如 ‘每日任务’ ‘每周任务’ 等选项,用户可以根据需要设置定时任务重复的日期范围,自动完成预约,签到,续约等操作。
不过以上功能的实现,需要一定的时间和精力测试与调试,也需要考虑到图书馆的正常运行,所以暂时不会着手开发,也许会进行一些小的功能验证。
#### 其他功能建议?
如果有其他功能建议,或者遇到了什么功能性,操作上的问题,欢迎联系我。
如果你有足够的开发能力,可以 Fork 本项目,根据自己的需求进行修改和完善,也欢迎提交 Pull Request 到本项目。
### 联系我
- 项目维护:[KenanZhu (Nanoki)](https://github.com/KenanZhu)
- 电子邮箱:<nanoki_zh@163.com>
_**Free to use** —— AutoLibrary 是一个基于 MIT 协议免费开源的工具_
BIN
View File
Binary file not shown.
View File
+1 -1
View File
@@ -9,7 +9,7 @@ See the LICENSE file for details.
"""
import queue
from MsgBase import MsgBase
from base.MsgBase import MsgBase
class LibOperator(MsgBase):
+2 -2
View File
@@ -29,7 +29,7 @@ class MsgBase:
msg: str
):
self._output_queue.put(f"[{self._class_name:<12}] >>> : {msg}")
self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}")
def _showTrace(
@@ -38,7 +38,7 @@ class MsgBase:
):
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(
+8
View File
@@ -0,0 +1,8 @@
"""
Base module for the AutoLibrary project.
Here are the classes and modules in this package:
- MsgBase: Base class for messages.\
- LibOperator: Base class for library operators.
"""
+141
View File
@@ -0,0 +1,141 @@
# -*- 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 sys
import platform
from PySide6.QtGui import (
QIcon
)
from PySide6.QtWidgets import (
QDialog, QApplication
)
from PySide6.QtCore import (
QTimer, Qt
)
from gui.AppInfo import AL_VERSION
from gui.Ui_ALAboutDialog import Ui_ALAboutDialog
from gui import AutoLibraryResource
class ALAboutDialog(QDialog, Ui_ALAboutDialog):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi(self)
self.modifyUi()
self.connectSignals()
def modifyUi(
self
):
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48))
info_text = self.generateAboutText()
self.AboutInfoEdit.setHtml(info_text)
self.AboutInfoEdit.setTextInteractionFlags(Qt.TextBrowserInteraction)
def connectSignals(
self
):
self.CopyButton.clicked.connect(self.copyAboutInfo)
def generateAboutText(
self
):
os_info = self.getOSInfo()
about_text = f"""
<h4>Version Information:</h4>
Version: {AL_VERSION}<br>
Python version: {platform.python_version()}<br>
Qt version: {self.getQtVersion()}<br>
<h4>System Information:</h4>
Processor: {platform.processor()}<br>
Operating system: {os_info['system']}<br>
System version: {os_info['version']}<br>
System architecture: {os_info['architecture']}<br>
<h4>Project Information:</h4>
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 website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br>
<h4>Author Information:</h4>
Developer: KenanZhu<br>
Contact: nanoki_zh@163.com<br>
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><br>
"""
return about_text
def getOSInfo(
self
):
system = platform.system()
version = platform.version()
architecture = platform.architecture()[0]
if system == "Windows":
try:
version = platform.win32_ver()[1]
except:
pass
elif system == "Darwin":
try:
version = platform.mac_ver()[0]
except:
pass
elif system == "Linux":
try:
import distro # try to get Linux distro info
version = f"{distro.name()} {distro.version()}"
except ImportError:
pass
return {
'system': system,
'version': version,
'architecture': architecture
}
def getQtVersion(
self
):
try:
from PySide6.QtCore import qVersion
return qVersion()
except:
return "Unknown"
def copyAboutInfo(
self
):
about_text = self.AboutInfoEdit.toPlainText()
clipboard = QApplication.clipboard()
clipboard.setText(about_text)
original_text = self.CopyButton.text()
self.CopyButton.setText("已复制")
QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text))
+175
View File
@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALAboutDialog</class>
<widget class="QDialog" name="ALAboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>关于 - AutoLibrary</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="ALAboutDialogLayout">
<property name="spacing">
<number>5</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>
<layout class="QHBoxLayout" name="LogoLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="LogoIconLabel">
<property name="minimumSize">
<size>
<width>56</width>
<height>56</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>56</width>
<height>56</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="LogoTextLabel">
<property name="font">
<font>
<pointsize>24</pointsize>
<bold>true</bold>
</font>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>AutoLibrary</string>
</property>
<property name="margin">
<number>0</number>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
</layout>
</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>
<widget class="QTextEdit" name="AboutInfoEdit">
<property name="font">
<font>
<family>Courier New</family>
<bold>false</bold>
</font>
</property>
<property name="lineWrapMode">
<enum>QTextEdit::LineWrapMode::NoWrap</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="CopyButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>复制</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+150
View File
@@ -0,0 +1,150 @@
# -*- 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 sys
import time
import uuid
import queue
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QDateTime
)
from PySide6.QtWidgets import (
QLabel, QDialog, QWidget, QSpinBox, QVBoxLayout,
QHBoxLayout, QGridLayout, QDateTimeEdit
)
from PySide6.QtGui import (
QCloseEvent
)
from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog
class TimerTaskStatus(Enum):
PENDING = "等待中"
READY = "已就绪"
RUNNING = "执行中"
EXECUTED = "已执行"
ERROR = "执行失败"
OUTDATED = "已过期"
class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi(self)
self.connectSignals()
self.modifyUi()
def modifyUi(
self
):
self.TimerTypeComboBox.setCurrentIndex(0)
self.SpecificTimerWidget = QWidget()
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
self.SpecificDateTimeEdit = QDateTimeEdit()
self.SpecificDateTimeEdit.setCalendarPopup(True)
self.SpecificDateTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
self.SpecificDateTimeEdit.setMinimumDateTime(QDateTime.currentDateTime())
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0)
self.RelativeDaySpinBox = QSpinBox()
self.RelativeDaySpinBox.setMinimum(0)
self.RelativeDaySpinBox.setMaximum(365)
self.RelativeDaySpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0)
self.RelativeHourSpinBox = QSpinBox()
self.RelativeHourSpinBox.setMinimum(0)
self.RelativeHourSpinBox.setMaximum(23)
self.RelativeHourSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1)
self.RelativeMinuteSpinBox = QSpinBox()
self.RelativeMinuteSpinBox.setMinimum(0)
self.RelativeMinuteSpinBox.setMaximum(59)
self.RelativeMinuteSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2)
self.RelativeSecondSpinBox = QSpinBox()
self.RelativeSecondSpinBox.setMinimum(0)
self.RelativeSecondSpinBox.setMaximum(59)
self.RelativeSecondSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3)
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False)
def connectSignals(
self
):
self.CancelButton.clicked.connect(self.reject)
self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
def getTimerTask(
self
) -> dict:
added_time = datetime.now()
if not self.TaskNameLineEdit.text():
name = f"未命名任务-{added_time.strftime("%Y%m%d%H%M%S")}"
else:
name = self.TaskNameLineEdit.text()
timer_type_index = self.TimerTypeComboBox.currentIndex()
silent = not self.ShowBeforeRunRadioButton.isChecked()
if timer_type_index == 0:
execute_time = self.SpecificDateTimeEdit.dateTime()
tmp_time_str = execute_time.toString("yyyy-MM-dd HH:mm:ss")
execute_time = datetime.strptime(tmp_time_str, "%Y-%m-%d %H:%M:%S")
else:
execute_time = datetime.now() + timedelta(
days = self.RelativeDaySpinBox.value(),
hours = self.RelativeHourSpinBox.value(),
minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value()
)
return {
"name": name,
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(),
"execute_time": execute_time,
"silent": silent,
"add_time": added_time,
"status": TimerTaskStatus.PENDING,
"executed": False
}
@Slot(int)
def onTimerTypeComboBoxIndexChanged(
self,
index: int
):
self.SpecificTimerWidget.setVisible(index == 0)
self.RelativeTimerWidget.setVisible(index == 1)
+249
View File
@@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALAddTimerTaskDialog</class>
<widget class="QDialog" name="ALAddTimerTaskDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>添加定时任务 - AutoLibrary</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
<property name="spacing">
<number>5</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>
<layout class="QHBoxLayout" name="TaskNameLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TaskNameLabel">
<property name="minimumSize">
<size>
<width>60</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="QLineEdit" name="TaskNameLineEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="TimerConfigGroupBox">
<property name="title">
<string>定时设置</string>
</property>
<layout class="QVBoxLayout" name="TimerConfigLayout">
<property name="spacing">
<number>5</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>
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TimerTypeLabel">
<property name="text">
<string>定时类型:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="TimerTypeComboBox">
<item>
<property name="text">
<string>特定时间</string>
</property>
</item>
<item>
<property name="text">
<string>相对时间</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="TaskConfigGroupBox">
<property name="title">
<string>运行设置</string>
</property>
<layout class="QGridLayout" name="TaskConfigLayout">
<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>
<property name="spacing">
<number>5</number>
</property>
<item row="0" column="0">
<widget class="QRadioButton" name="SilentlyRunRadioButton">
<property name="text">
<string>静默运行</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoRepeat">
<bool>false</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
<property name="text">
<string>运行前提示</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ControLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<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="QPushButton" name="CancelButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>取消</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ConfirmButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+419
View File
@@ -0,0 +1,419 @@
# -*- 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 sys
import time
import queue
from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
)
from PySide6.QtWidgets import (
QMainWindow, QMenu, QSystemTrayIcon
)
from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
)
from gui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.ALConfigWidget import ALConfigWidget
from gui.ALTimerTaskWidget import ALTimerTaskWidget
from gui.ALAboutDialog import ALAboutDialog
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
from gui import AutoLibraryResource
from utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
timerTaskIsRunning = Signal(dict)
timerTaskIsExecuted = Signal(dict)
timerTaskIsError = Signal(dict)
def __init__(
self
):
super().__init__()
self.__class_name = self.__class__.__name__
self.setupUi(self)
self.__input_queue = queue.Queue()
self.__output_queue = queue.Queue()
self.__timer_task_queue = queue.Queue()
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__config_paths = {
"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
self.__auto_lib_thread = None
self.__current_timer_task_thread = None
self.__is_running_timer_task = False
self.modifyUi()
self.setupTray()
self.connectSignals()
self.startMsgPolling()
self.startTimerTaskPolling()
def modifyUi(
self
):
self.icon = QIcon(":/res/icon/icons/AutoLibrary_32x32.ico")
self.setWindowIcon(self.icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered)
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(
self
):
about_dialog = ALAboutDialog(self)
about_dialog.exec()
def onManualActionTriggered(
self
):
url = QUrl("https://www.autolibrary.cv/docs/manual_lists.html")
QDesktopServices.openUrl(url)
def setupTray(
self
):
if not QSystemTrayIcon.isSystemTrayAvailable():
self.showTraceSignal.emit(
"系统不支持系统托盘功能, 无法创建系统托盘图标。"
)
return
self.TrayIcon = QSystemTrayIcon(self.icon, self)
self.TrayIcon.setToolTip("AutoLibrary")
self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.show()
def hideToTray(
self
):
self.hide()
self.TrayIcon.showMessage(
"AutoLibrary",
"\n已最小化到托盘",
QSystemTrayIcon.MessageIcon.Information,
2000
)
def onTrayIconActivated(
self,
reason: QSystemTrayIcon.ActivationReason
):
if reason == QSystemTrayIcon.DoubleClick:
self.showNormal()
def connectSignals(
self
):
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked)
self.StartButton.clicked.connect(self.onStartButtonClicked)
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
def closeEvent(
self,
event: QCloseEvent
):
if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
self.__msg_queue_timer.stop()
if self.__timer_task_timer and self.__timer_task_timer.isActive():
self.__timer_task_timer.stop()
if self.__is_running_timer_task:
self.__current_timer_task_thread.wait(2000)
self.__current_timer_task_thread.deleteLater()
if self.__alTimerTaskWidget:
self.__alTimerTaskWidget.close()
self.__alTimerTaskWidget.deleteLater()
if self.__alConfigWidget:
self.__alConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
super().closeEvent(event)
def appendToTextEdit(
self,
text: str
):
cursor = self.MessageIOTextEdit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(text + "\n")
self.MessageIOTextEdit.setTextCursor(cursor)
self.MessageIOTextEdit.ensureCursorVisible()
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def startMsgPolling(
self
):
self.__msg_queue_timer = QTimer()
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
self.__msg_queue_timer.start(100)
def startTimerTaskPolling(
self
):
self.__timer_task_timer = QTimer()
self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
self.__timer_task_timer.start(500)
def pollTimerTaskQueue(
self
):
if self.__is_running_timer_task:
return
try:
while not self.__is_running_timer_task:
timer_task = self.__timer_task_queue.get_nowait()
self.timerTaskIsRunning.emit(timer_task)
self.__timer_task_timer.stop()
self.__is_running_timer_task = True
self.setControlButtons(True, True, False)
if not timer_task["silent"]:
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n已开始执行定时任务: \n{timer_task['name']}",
QSystemTrayIcon.MessageIcon.Information,
1000
)
self.showNormal()
self.__current_timer_task_thread = TimerTaskWorker(
timer_task,
self.__input_queue,
self.__output_queue,
self.__config_paths
)
self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.start()
except queue.Empty:
self.__is_running_timer_task = False
pass
def setControlButtons(
self,
config_button_enabled: bool,
stop_button_enabled: bool,
start_button_enabled: bool
):
self.ConfigButton.setEnabled(config_button_enabled)
self.StopButton.setEnabled(stop_button_enabled)
self.StartButton.setEnabled(start_button_enabled)
@Slot()
def showMsg(
self,
msg: str
):
self.__output_queue.put(f"[{self.__class_name:<15}] >>> : {msg}")
@Slot()
def showTrace(
self,
msg: str
):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.__output_queue.put(f"{timestamp}-[{self.__class_name:<15}] : {msg}")
@Slot()
def pollMsgQueue(
self
):
try:
while True:
msg = self.__output_queue.get_nowait()
self.appendToTextEdit(msg)
except queue.Empty:
pass
@Slot()
def onTimerTaskWidgetClosed(
self
):
self.TimerTaskWidgetButton.setEnabled(True)
@Slot(dict)
def onConfigWidgetClosed(
self,
config_paths: dict
):
if self.__alConfigWidget:
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
self.setControlButtons(True, False, True)
self.__config_paths = config_paths
@Slot(dict)
def onTimerTaskIsReady(
self,
timer_task: dict
):
self.__timer_task_queue.put(timer_task)
@Slot(dict)
def onTimerTaskFinished(
self,
is_error: bool,
timer_task: dict
):
self.__current_timer_task_thread.wait(1000)
self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.deleteLater()
self.__current_timer_task_thread = None
self.setControlButtons(True, False, True)
self.__is_running_timer_task = False
self.__timer_task_timer.start(500)
timer_task["executed"] = True
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
QSystemTrayIcon.MessageIcon.Information,
1000
)
self.showTrace(
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}"
)
if not is_error:
self.timerTaskIsExecuted.emit(timer_task)
else:
self.timerTaskIsError.emit(timer_task)
@Slot()
def onTimerTaskWidgetButtonClicked(
self
):
self.__alTimerTaskWidget.show()
self.__alTimerTaskWidget.raise_()
self.__alTimerTaskWidget.activateWindow()
self.TimerTaskWidgetButton.setEnabled(False)
@Slot()
def onConfigButtonClicked(
self
):
if self.__alConfigWidget is None:
self.__alConfigWidget = ALConfigWidget(
self,
self.__config_paths
)
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.show()
self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow()
self.ConfigButton.setEnabled(False)
@Slot()
def onStartButtonClicked(
self
):
self.setControlButtons(True, True, False)
if self.__auto_lib_thread is None:
self.__auto_lib_thread = AutoLibWorker(
self.__input_queue,
self.__output_queue,
self.__config_paths
)
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.finishedWithErrorSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.start()
@Slot()
def onStopButtonClicked(
self
):
if self.__auto_lib_thread:
self.showTrace("正在停止操作......")
self.__auto_lib_thread.wait(2000)
self.showTrace("操作已停止")
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 = None
self.setControlButtons(True, False, True)
@Slot()
def onSendButtonClicked(
self
):
msg = self.MessageEdit.text().strip()
if not msg:
return
self.showMsg(msg)
self.MessageEdit.clear()
@@ -50,11 +50,33 @@
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="TimerTaskWidgetButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="document-open-recent"/>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<property name="minimumSize">
<size>
<width>1280</width>
<width>1000</width>
<height>0</height>
</size>
</property>
@@ -237,6 +259,9 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
<property name="text">
<string>发送</string>
</property>
<property name="icon">
<iconset theme="document-send"/>
</property>
</widget>
</item>
</layout>
@@ -245,7 +270,7 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
</widget>
<widget class="QMenuBar" name="MenuBar">
<property name="enabled">
<bool>false</bool>
<bool>true</bool>
</property>
<property name="geometry">
<rect>
@@ -258,12 +283,33 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
<property name="nativeMenuBar">
<bool>true</bool>
</property>
<widget class="QMenu" name="HelpMenu">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="title">
<string>帮助</string>
</property>
<addaction name="ManualAction"/>
<addaction name="AboutAction"/>
</widget>
<addaction name="HelpMenu"/>
</widget>
<widget class="QStatusBar" name="StatusBar">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
<action name="ManualAction">
<property name="text">
<string>在线手册</string>
</property>
</action>
<action name="AboutAction">
<property name="text">
<string>关于</string>
</property>
</action>
</widget>
<resources/>
<connections/>
+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)
+99
View File
@@ -0,0 +1,99 @@
# -*- 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 PySide6.QtCore import (
Qt, Signal
)
from PySide6.QtWidgets import (
QFrame, QLabel
)
class ALSeatFrame(QFrame):
clicked = Signal(str)
def __init__(
self,
seat_number,
parent=None
):
super().__init__(parent)
self.__seat_number = seat_number
self.__is_selected = False
self.setupUi()
def setupUi(
self
):
self.setFixedSize(60, 40)
self.setFrameStyle(QFrame.Box | QFrame.Plain)
self.setLineWidth(2)
self.setStyleSheet("""
QFrame {
background-color: #4196EB;
border: 2px solid #4196EB;
border-radius: 5px;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
self.label = QLabel(self.__seat_number, self)
self.label.setAlignment(Qt.AlignCenter)
self.label.setGeometry(0, 0, 60, 40)
def mousePressEvent(
self,
event
):
if event.button() == Qt.LeftButton:
self.toggleSelection()
self.clicked.emit(self.__seat_number)
def isSelected(
self
):
return self.__is_selected
def toggleSelection(self):
self.__is_selected = not self.__is_selected
if self.__is_selected:
self.setStyleSheet("""
QFrame {
background-color: #4CAF50;
border: 2px solid #388E3C;
border-radius: 5px;
color: white;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
else:
self.setStyleSheet("""
QFrame {
background-color: #4196EB;
border: 2px solid #4196EB;
border-radius: 5px;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
+270
View File
@@ -0,0 +1,270 @@
seats_maps = {
"2": {
"1": """
,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,,
,,,,,,,,,,,039C,039D,,040C,040D,,041C,041D,,042C,042D,,043C,043D,,044C,044D,,,,,,,,,
038B,038D,,037B,037D,,036B,036D,,,,,,,,,,,,,,,,,,,,,,045C,045A,,046C,046A,,047C,047A
038A,038C,,037A,037C,,036A,036C,,,,,,,,,,,,,,,,,,,,,,045D,045B,,046D,046B,,047D,047B
035B,035D,,034B,034D,,033B,033D,,,,,,,,,,,,,,,,,,,,,,048C,048A,,049C,049A,,050C,050A
035A,035C,,034A,034C,,033A,033C,,,,,,,,,,,,,,,,,,,,,,048D,048B,,049D,049B,,050D,050B
032B,032D,,031B,031D,,030B,030D,,,,,,,,,,,,,,,,,,,,,,051C,051A,,052C,052A,,053C,053A
032A,032C,,031A,031C,,030A,030C,,,,,,,,,,,,,,,,,,,,,,051D,051B,,052D,052B,,053D,053B
029B,029D,,028B,028D,,027B,027D,,,,,,,,,,,,,,,,,,,,,,054C,054A,,055C,055A,,056C,056A
029A,029C,,028A,028C,,027A,027C,,,,,,,,,,,,,,,,,,,,,,054D,054B,,055D,055B,,056D,056B
026B,026D,,025B,025D,,024B,024D,,,,,,,,,,,,,,,,,,,,,,057C,057A,,058C,058A,,059C,059A
026A,026C,,025A,025C,,024A,024C,,,,,,,,,,,,,,,,,,,,,,057D,057B,,058D,058B,,059D,059B
023B,023D,,022B,022D,,021B,021D,,,,,,,,,,,,,,,,,,,,,,060C,060A,,061C,061A,,062C,062A
023A,023C,,022A,022C,,021A,021C,,,,,,,,,,,,,,,,,,,,,,060D,060B,,061D,061B,,062D,062B
020B,020D,,019B,019D,,018B,018D,,,,,,,,,,,,,,,,,,,,,,063C,063A,,064C,064A,,065C,065A
020A,020C,,019A,019C,,018A,018C,,,,,,,,,,,,,,,,,,,,,,063D,063B,,064D,064B,,065D,065B
,,,,,,,,,,,017D,017C,,014D,014C,,011D,011C,,008D,008C,,005D,005C,,002D,002C,001D,001C,,,,,,,
,,,,,,,,,,,017B,017A,,014B,014A,,011B,011A,,008B,008A,,005B,005A,,002B,002A,001B,001A,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,073D,073C,,015D,015C,,012D,012C,,,,,006D,006C,,003D,003C,,,,,,,,,
,,,,,,,,,,,073B,073A,,015B,015A,,012B,012A,,,,,006B,006A,,003B,003A,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,072D,072C,,016D,016C,,013D,013C,,,,,007D,007C,,004D,004C,,,,,,,,,
,,,,,,,,,,,072B,072A,,016B,016A,,013B,013A,,,,,007B,007A,,004B,004A,,,,,,,,,
,,,,,,,,,,,071D,071C,,070D,070C,,069D,069C,,068D,068C,,067D,067C,,066D,066C,,,,,,,,,
,,,,,,,,,,,071B,071A,,070B,070A,,069B,069A,,068B,068A,,067B,067A,,066B,066A,,,,,,,,,
""",
"2": """
023B,023D,024B,024D,,,,,,,,,,,,,,,
023A,023C,024A,024C,,,,,,,,,,,,,,,
022B,022D,032D,032C,,,,,,,,,,,,,,,
022A,022C,032B,032A,,,,,,,,,,,,,,,
021B,021D,,,,,,,,,,,,,,,,,
021A,021C,,,,,,,,,,,,,,,,,
020B,020D,,,,,,,,,,,,,,,,,
020A,020C,,,,,,,,,,,,,,,,,
019B,019D,,,,,,,,,,,,,,,,,
019A,019C,,,,,,,,,,,,,,,,,
018B,018D,,,,,,,,,,,,,,,,,
018A,018C,,,,,,,,,,,,,,,,,
017B,017D,,,,,,,,,,,,,,,,,
017A,017C,,,,,,,,,,,,,,,,,
016B,016D,,,,,,,,,,,,,,,,,
016A,016C,,,,,031A,031C,,,,,,,,,,,
015B,015D,,,,,030B,030D,,,,,,,,,,,
015A,015C,,,,,030A,030C,,,,,,,,,,,
014B,014D,,,,,029B,029D,,,,,,,,,,,
014A,014C,,,,,029A,029C,,,,,,,,,,,
013B,013D,,,,,028B,028D,,,,,,,,,,,
013A,013C,,,,,028A,028C,,,,,,,,,,,
012B,012D,,,,,027B,027D,,,,,,,,,,,
012A,012C,,,,,027A,027C,,,,,,,,,,,
011B,011D,,,,,026B,026D,,,,,,,,,,,
011A,011C,,,,,026A,026C,,,,,,,,,,,
010B,010D,,,,,025B,025D,,,,,,,,,,,
010A,010C,,,,,,,,,,,,,,,,,
009B,009D,,,,,,,,,,,,,,,,,
009A,009C,,,,,,,,,,,,,,,,,
008B,008D,,,,,,,,,,,,,,,,,
008A,008C,,,,,,,,,,,,,,,,,
007B,007D,,,,,,,,,,,,,,,,,
007A,007C,,,,,,,,,,,,,,,,,
006B,006D,,,,,,,,,,,,,,,,,
006A,006C,,,,,,,,,,,,,,,,,
005B,005D,,,,,,,,,,,,,,,,,
005A,005C,,,,,,,,,,,,,,,,,
004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,
004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,
"""
},
"3": {
"3": """
,,007B,007D,,,,,,,,008C,008A,,
,,007A,007C,,,,,,,,008D,008B,,
,,006B,006D,,,,,,,,009C,009A,,
,,006A,006C,,,,,,,,009D,009B,,
,,005B,005D,,,,,,,,010C,010a,,
,,005A,005C,,,,,,,,010D,010B,,
,,004B,004D,,,,,,,,011C,011A,,
,,004A,004C,,,,,,,,011D,011B,,
,,003B,003D,,,,,,,,012C,012A,,
,,003A,003C,,,,,,,,012D,012B,,
,,002B,002D,,,,,,,,013C,013A,,
,,002A,002C,,,,,,,,013D,013B,,
,,001B,001D,,,,,,,,014C,014A,,
,,001A,001C,,,,,,,,014D,014B,,
""",
"4": """
,,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,056D,056C,057D,057C,,
,,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,056B,056A,057B,057A,,
036B,036D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058C,058A,,060C,060A
036A,036C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058D,058B,,060D,060B
035B,035D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059C,059A,,061C,061A
035A,035C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059D,059B,,061D,061B
034B,034D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062C,062A
034A,034C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062D,062B
033B,033D,,,,,,,,,,,,080B,080D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,063C,063A
033A,033C,,,,,,,,,,,,080A,080C,,081A,081B,082A,082B,083A,083B,084A,084B,085A,085B,086A,086B,087A,,,,,,,,,,,,,,,,,,063D,063B
032B,032D,,,,,,,,,,,,079B,079D,,081C,081D,082C,082D,083C,083D,084C,084D,085C,085D,086C,086D,087C,,,,,,,,,,,,,,,,,,064C,064A
032A,032C,,,,,,,,,,,,079A,079C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,064D,064B
031B,031D,,,,,,,,,,,,078B,078D,,,,,,,,,,,,,,088A,088C,,,,,,,,,,,,,,,,,065C,065A
031A,031C,,,,,,,,,,,,078A,078C,,,,,,,,,,,,,,088B,088D,,,,,,,,,,,,,,,,,065D,065B
030B,030D,,,,,,,,,,,,077B,077D,,,,,,,,,,,,,,089A,089C,,,,,,,,,,,,,,,,,066C,066A
030A,030C,,,,,,,,,,,,077A,077C,,,,,,,,,,,,,,089B,089D,,,,,,,,,,,,,,,,,066D,066B
029B,029D,,,,,,,,,,,,076B,076D,,,,,,,,,,,,,,090A,090C,,,,,,,,,,,,,,,,,,
029A,029C,,,,,,,,,,,,076A,076C,,,,,,,,,,,,,,090B,090D,,,,,,,,,,,,,,,,,,
028B,028D,,,,,,,,,,,,075B,075D,,,,,,,,,,,,,,091A,091C,,,,,,,,,,,,,,,,,,
028A,028C,,,,,,,,,,,,075A,075C,,,,,,,,,,,,,,091B,091D,,,,,,,,,,,,,,,,,,
027B,027D,,,,,,,,,,,,074B,074D,,,,,,,,,,,,,,092A,092C,,,,,,,,,,,,,,,,,,
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,092B,092D,,,,,,,,,,,,,,,,,,
026B,026D,,,,,,,,,,,,,,,073D,073C,072D,072C,071D,071C,070D,070C,069D,069C,068D,068C,,,,,,,,,,,,,,,,,,,,
026A,026C,,,,,,,,,,,,,,,073B,073A,072B,072A,071B,071A,070B,070A,069B,069A,068B,068A,,,,,,,,,,,,,,,,,,,,
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
024B,024D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
024A,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
023B,023D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
023A,023C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067C,,
022B,022D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067B,,
022A,022C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067A,,
,,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,
,,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007a,006B,006A,005B,005A,004B,004A,003b,003A,002B,002A,001B,001A,,,,
"""
},
"4": {
"5": """
,,,,,,,,042A,042B,045A,045B,048A,048B,051A,051B,054A,054B,057A,057B,060A,060B,,,,,,
,,,,,,,,042C,042D,045C,045D,048C,048D,051C,051D,054C,054D,057C,057D,060C,060D,,,,,,
,,,,,,,,041A,041B,044A,044B,047A,047B,050A,050B,053A,053B,056A,056B,059A,059B,,,,,,
,,,,,,,,041C,041D,044C,044D,047C,047D,050C,050D,053C,053D,056C,056D,059C,059D,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,040A,040B,043A,043B,046A,046B,049A,049B,052A,052B,055A,055B,058A,058B,,,,,,
,,,,,,,,040C,040D,043C,043D,046C,046D,049C,049D,052C,052D,055C,055D,058C,058D,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,039B,039D,038B,038D,,037B,037D,,,,,,,,,,,,,,,,,,,,,
,039A,039C,038A,038C,,037A,037C,,,,,,,,,,,,,,,,,,,,,
,036B,036D,035B,035D,,034B,034D,,,,,,,,,,,,,,,,,,,,,
,036A,036C,035A,035C,,034A,034C,,,,,,,,,,,,,,,,,,,,,
,033B,033D,032B,032D,,031B,031D,,,,,,,,,,,,,,,,,,,,,
,033A,033C,032A,032C,,031A,031C,,,,,,,,,,,,,,,,,,,,,
,030B,030D,029B,029D,,028B,028D,,,,,,,,,,,,,,,,,,,,,
,030A,030C,029A,029C,,028A,028C,,,,,,,,,,,,,,,,,,,,,
,027B,027D,026B,026D,,025B,025D,,,,,,,,,,,,,,,,,,,,,
,027A,027C,026A,026C,,025A,025C,,,,,,,,,,,,,,,,,,,,,
,024B,024D,023B,023D,,022B,022D,,,,,,,,,,,,,,,,,,,,,
,024A,024C,023A,023C,,022A,022C,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,019D,019C,016D,016C,013D,013C,010D,010C,007D,007C,004D,004C,001D,001C,,,,,,
,,,,,,,,019B,019A,016B,016A,013B,013A,010B,010A,007B,007A,004B,004A,001B,001A,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,020D,020C,017D,017C,014D,014C,011D,011C,008D,008C,005D,005C,002D,002C,,,,,,
,,,,,,,,020B,020A,017B,017A,014B,014A,011B,011A,008B,008A,005B,005A,002B,002A,,,,,,
,,,,,,,,021D,021C,018D,018C,015D,015C,012D,012C,009D,009C,006D,006C,003D,003C,,,,,,
,,,,,,,,021B,021A,018B,018A,015B,015A,012B,012A,009B,009A,006B,006A,003B,003A,,,,,,
""",
"6": """
,,,026C,026D,027D,027C,028D,028C,029D,029C,030D,030C,031D,031C,032D,032C,033D,033C,035D,035C,036D,036C,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,046D,046C
,,,026A,026B,027B,027A,028B,028A,029B,029A,030B,030A,031B,031A,032B,032A,033B,033A,035B,035A,036B,036A,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,046B,046A
025D,025C,,,,,,,,,,,,,,,,034D,034C,,,,,,,,,,,,,,,,,,,,,,,047C,047A
025B,025A,,,,,,,,,,,,,,,,034B,034A,,,,,,,,,,,,,,,,,,,,,,,047D,047B
024D,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,048C,048A
024B,024A,,,,,,,,,,,,,,050D,050C,052D,052C,054D,054C,056D,056C,058D,058C,060D,060C,,,,,,,,,,,,,,,048D,048B
023D,023C,,,,,,,,,,,,,,050B,050A,052B,052A,054B,054A,056B,056A,058B,058A,060B,060A,,,,,,,,,,,,,,,,
023B,023A,,,,,,,,,,,,,,049D,049C,051D,051C,053D,053C,055D,055C,057D,057C,059D,059C,,,,,,,,,,,,,,,,
022D,022C,,,,,,,,,,,,,,049B,049A,051B,051A,053B,053A,055B,055A,057B,057A,059B,059A,,,,,,,,,,,,,,,,
022B,022A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
021D,021C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
021B,021A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
020D,020C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
020B,020A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
019D,019C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
019B,019A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
015D,015C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
015B,015A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
014D,014C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
014B,014A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
013D,013C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
013B,013A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
012D,012C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
012B,012A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
011D,011C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
011B,011A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
010D,010C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
010B,010A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
009D,009C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
009B,009A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
008D,008C,,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,,,,,,,,,,,,,,,,
008B,008A,,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,,,,,,,,,,,,,,,,
""",
"7": """
,,,,,,,,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,,,,,,,,,,,,
,,,,,,,,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
"""
},
"5": {
"8": """
,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,056C,056A,,,,,,,,,,,,,,,,,,,,,,,,,,
045B,045D,,,,,,,,,,,,,,,,,,,,,,056D,056B,,,,,,,,,,,,,,,,,,,,,,,,,,
045A,045C,,,,,,,,,,,,,,,,,,,,,,057C,057A,,,,,,,,,,,,,,,,,,,,,,,,,,
044B,044D,,,,,,,,,,,,,,,,,,,,,,057D,057B,,,,,,,,,,,,,,,,,,,,,,,,,,
044A,044C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
043B,043D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
043A,043C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
042B,042D,,,,,,,,,,,,,,,,,070B,070D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
042A,042C,,,,,,,,,,,,,,,,,070A,070C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
041B,041D,,,,,,,,,,,,,,,,,069B,069D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
041A,041C,,,,,,,,,,,,,,,,,069A,069C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
040B,040D,,,,,,,,,,,,,,,,,068B,068D,,071A,071B,072A,072B,073A,073B,074A,074B,075A,075B,076A,076B,077A,077B,,,,,,,,,,,,,,,,
040A,040C,,,,,,,,,,,,,,,,,068A,068C,,071C,071D,072C,072D,073C,073D,074C,074D,075C,075D,076C,076D,077C,077D,,,,,,,,,,,,,,,,
039B,039D,,,,,,,,,,,,,,,,,067B,067D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
039A,039C,,,,,,,,,,,,,,,,,067A,067C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
038B,038D,,,,,,,,,,,,,,,,,066B,066D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
038A,038C,,,,,,,,,,,,,,,,,066A,066C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
037B,037D,,,,,,,,,,,,,,,,,065B,065D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
037A,037C,,,,,,,,,,,,,,,,,065A,065C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
036B,036D,,,,,,,,,,,,,,,,,064B,064D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
036A,036C,,,,,,,,,,,,,,,,,064A,064C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
035B,035D,,,,,,,,,,,,,,,,,063B,063D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
035A,035C,,,,,,,,,,,,,,,,,063A,063C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
034B,034D,,,,,,,,,,,,,,,,,062B,062D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
034A,034C,,,,,,,,,,,,,,,,,062A,062C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
033B,033D,,,,,,,,,,,,,,,,,,,061D,061C,,060D,060C,,059D,059C,,058D,058C,,,,,,,,,,,,,,,,,,,,
033A,033C,,,,,,,,,,,,,,,,,,,061B,061A,,060B,060A,,059B,059A,,058B,058A,,,,,,,,,,,,,,,,,,,,
032B,032D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
032A,032C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
031B,031D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
031A,031C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
030B,030D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
030A,030C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
029B,029D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
029A,029C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
028B,028D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
028A,028C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
027B,027D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
026B,026D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
026A,026C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,024D,024C,023D,023C,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
,,,024B,024A,023B,023A,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
"""
}
}
+276
View File
@@ -0,0 +1,276 @@
# -*- 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 PySide6.QtCore import (
Qt, Slot, Signal, QEvent
)
from PySide6.QtWidgets import (
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout,
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem,
QPushButton,
)
from PySide6.QtGui import (
QPainter, QWheelEvent, QCloseEvent
)
from gui.ALSeatFrame import ALSeatFrame
class ALSeatMapWidget(QWidget):
seatMapWidgetClosed = Signal(list)
def __init__(
self,
parent: QWidget = None,
floor: str = "",
room: str = "",
seats_data: dict = {},
):
super().__init__(parent)
self.__floor = floor
self.__room = room
self.__seats_data = seats_data
self.__selected_seats = []
self.__seat_frames = {}
self.setupUi()
self.connectSignals()
@staticmethod
def formatSeatNumber(
seat_number: str
) -> str:
if seat_number and not seat_number[-1].isdigit():
digits = seat_number[:-1]
letter = seat_number[-1]
return digits.zfill(3) + letter
return seat_number.zfill(3)
def setupUi(
self
):
self.setWindowFlags(Qt.WindowType.Window)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(800, 600)
self.resize(800, 600)
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
self.SeatMapGraphicsView = QGraphicsView(self)
self.SeatMapGraphicsScene = QGraphicsScene(self)
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.viewport().installEventFilter(self)
self.SeatsContainerWidget = QWidget()
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
self.createSeatMap()
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
self.TipsLabel = QLabel(
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
)
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.SeatMapWidgetControlLayout = QHBoxLayout()
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
def connectSignals(
self
):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
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(
self,
event: QCloseEvent
):
self.seatMapWidgetClosed.emit(self.__selected_seats)
super().closeEvent(event)
def eventFilter(
self,
watched,
event
):
if (watched is self.SeatMapGraphicsView.viewport() and
event.type() == QEvent.Type.Wheel and
event.modifiers() == Qt.KeyboardModifier.ControlModifier
):
self.zoomGraphicsView(event)
return True
return super().eventFilter(watched, event)
def zoomGraphicsView(
self,
event: QWheelEvent
):
delta = event.angleDelta().y()
zoom_factor = 1.2 if delta > 0 else 1/1.2
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor)
def createSeatMap(
self
):
rows = self.__seats_data.strip().split("\n")
for row_idx, row in enumerate(rows):
col_idx = 0
seats_number = [seat.strip() for seat in row.split(",")]
for seat_number in seats_number:
if seat_number:
seat_widget = ALSeatFrame(seat_number)
seat_widget.clicked.connect(self.onSeatClicked)
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
self.__seat_frames[seat_number] = seat_widget
else:
spacer = QFrame()
spacer.setFixedSize(20, 30)
spacer.setStyleSheet("background-color: transparent; border: none;")
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
col_idx += 1
self.SeatsContainerLayout.setSpacing(20)
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
self.SeatsContainerWidget.adjustSize()
def selectSeat(
self,
seat_number: str
):
if len(self.__selected_seats) >= 1:
return
seat_number = self.formatSeatNumber(seat_number)
if seat_number not in self.__seat_frames:
return
widget = self.__seat_frames[seat_number]
if widget.isSelected():
return
widget.toggleSelection()
self.__selected_seats.append(seat_number)
def selectSeats(
self,
selected_seats: list
):
self.clearSelections()
for seat_number in selected_seats:
self.selectSeat(seat_number)
def getSelectedSeats(
self
) -> list[str]:
return self.__selected_seats
def clearSelections(
self
):
seats_to_clear = self.__selected_seats.copy()
for seat_number in seats_to_clear:
if seat_number not in self.__seat_frames:
continue
widget = self.__seat_frames[seat_number]
if widget.isSelected():
widget.toggleSelection()
self.__selected_seats = []
@Slot(str)
def onSeatClicked(
self,
seat_number: str
):
if seat_number in self.__selected_seats:
self.__selected_seats.remove(seat_number)
else:
if len(self.__selected_seats) < 1:
self.__selected_seats.append(seat_number)
else:
self.__seat_frames[seat_number].toggleSelection()
@Slot()
def onConfirmButtonClicked(
self
):
self.close()
@Slot()
def onCancelButtonClicked(
self
):
self.clearSelections()
self.close()
+509
View File
@@ -0,0 +1,509 @@
# -*- 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 sys
import time
import copy
import queue
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QTimer
)
from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox,
QHBoxLayout, QVBoxLayout, QLabel, QPushButton
)
from PySide6.QtGui import (
QCloseEvent, QScreen
)
from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget
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):
def __init__(
self,
parent = None,
timer_task: dict = None
):
super().__init__(parent)
self.__timer_task = timer_task
self.modifyUi()
def modifyUi(
self
):
self.ItemWidgetLayout = QHBoxLayout(self)
self.ItemWidgetLayout.setSpacing(10)
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
self.TaskInfoLayout = QVBoxLayout()
self.TaskInfoLayout.setSpacing(5)
TaskNameLabel = QLabel(self.__timer_task["name"])
TaskNameLabelFont = TaskNameLabel.font()
TaskNameLabelFont.setBold(True)
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}")
ExecuteTimeLabel.setStyleSheet("color: gray;")
ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
self.ItemWidgetLayout.addStretch()
match self.__timer_task["status"]:
case TimerTaskStatus.PENDING:
TaskStatusText = "等待中"
TaskStatusColor = "#FF9800"
case TimerTaskStatus.READY:
TaskStatusText = "已就绪"
TaskStatusColor = "#316BFF"
case TimerTaskStatus.RUNNING:
TaskStatusText = "执行中"
TaskStatusColor = "#2294FF"
case TimerTaskStatus.EXECUTED:
TaskStatusText = "已执行"
TaskStatusColor = "#4CAF50"
case TimerTaskStatus.ERROR:
TaskStatusText = "执行失败"
TaskStatusColor = "#FF5722"
case TimerTaskStatus.OUTDATED:
TaskStatusText = "已过期"
TaskStatusColor = "#FF5722"
TaskStatusLabel = QLabel(TaskStatusText)
TaskStatusLabel.setStyleSheet(f"""
QLabel {{
background-color: {TaskStatusColor};
color: white;
border-radius: 5px;
font-weight: bold;
}}
""")
TaskStatusLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
TaskStatusLabel.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(TaskStatusLabel)
TaskModeText = "静默" if self.__timer_task["silent"] else "显示"
TaskModeColor = "#6325FF" if self.__timer_task["silent"] else "#2294FF"
TaskModeLabel = QLabel(TaskModeText)
TaskModeLabel.setStyleSheet(f"""
QLabel {{
background-color: {TaskModeColor};
color: white;
border-radius: 5px;
font-weight: bold;
}}
""")
TaskModeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
TaskModeLabel.setFixedSize(60, 25)
self.ItemWidgetLayout.addWidget(TaskModeLabel)
self.DeleteButton = QPushButton("删除")
self.DeleteButton.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(self.DeleteButton)
if self.__timer_task["status"] == TimerTaskStatus.READY\
or self.__timer_task["status"] == TimerTaskStatus.RUNNING:
self.DeleteButton.setEnabled(False)
self.setFixedHeight(55)
class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
timerTasksChanged = Signal()
timerTaskIsReady = Signal(dict)
timerTaskWidgetClosed = Signal()
def __init__(
self,
parent = None,
timer_tasks_config_path: str = ""
):
super().__init__(parent)
self.__timer_tasks = []
self.__check_timer = None
self.__sort_policy = SortPolicy.BY_EXECUTE_TIME
self.__sort_order = Qt.SortOrder.AscendingOrder
self.__timer_tasks_config_path = timer_tasks_config_path
self.setupUi(self)
self.connectSignals()
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.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked)
self.timerTasksChanged.connect(self.onTimerTasksChanged)
def setupTimer(
self
):
self.__check_timer = QTimer(self)
self.__check_timer.timeout.connect(self.checkTasks)
self.__check_timer.start(500)
def initializeTimerTasks(
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
):
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(
self,
event: QCloseEvent
):
self.hide()
self.timerTaskWidgetClosed.emit()
event.ignore()
def sortTimerTasks(
self,
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME,
order: Qt.SortOrder = Qt.SortOrder.AscendingOrder
):
if policy == SortPolicy.BY_NAME:
self.__timer_tasks.sort(
key = lambda x: x["name"],
reverse = order is Qt.SortOrder.DescendingOrder
)
elif policy == SortPolicy.BY_ADD_TIME:
self.__timer_tasks.sort(
key = lambda x: x["add_time"],
reverse = order is Qt.SortOrder.DescendingOrder
)
elif policy == SortPolicy.BY_EXECUTE_TIME:
self.__timer_tasks.sort(
key = lambda x: x["execute_time"],
reverse = order is Qt.SortOrder.DescendingOrder
)
def updateStat(
self
):
pending = 0
in_queue = 0
executed = 0
invalid = 0
total = len(self.__timer_tasks)
for timer_task in self.__timer_tasks:
if timer_task["status"] == TimerTaskStatus.PENDING:
pending += 1
elif timer_task["status"] == TimerTaskStatus.READY\
or timer_task["status"] == TimerTaskStatus.RUNNING:
in_queue += 1
elif timer_task["status"] == TimerTaskStatus.EXECUTED:
executed += 1
elif timer_task["status"] == TimerTaskStatus.ERROR\
or timer_task["status"] == TimerTaskStatus.OUTDATED:
invalid += 1
self.TotalTaskLabel.setText(f"总任务:{total}")
self.PendingTaskLabel.setText(f"待执行:{pending}")
self.InQueueTaskLabel.setText(f"队列中:{in_queue}")
self.ExecutedTaskLabel.setText(f"已执行:{executed}")
self.InvalidTaskLabel.setText(f"无效的:{invalid}")
def updateTimerTaskList(
self
):
self.TimerTasksListWidget.clear()
self.sortTimerTasks(self.__sort_policy, self.__sort_order)
for timer_task in self.__timer_tasks:
item = QListWidgetItem()
item.setData(Qt.UserRole, timer_task)
widget = TimerTaskItemWidget(self, timer_task)
widget.DeleteButton.clicked.connect(
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid)
)
item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget)
def addTask(
self
):
dialog = ALAddTimerTaskWidget(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = dialog.getTimerTask()
self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit()
def deleteTask(
self,
task_uuid: str
):
self.__timer_tasks = [
x for x in self.__timer_tasks
if x["task_uuid"] != task_uuid
]
self.timerTasksChanged.emit()
def clearAllTasks(
self
):
if not self.__timer_tasks:
return
result = QMessageBox.question(
self,
"确认 - AutoLibrary",
"是否要清除所有定时任务 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if result is QMessageBox.StandardButton.No:
return
in_queue_tasks = [
x for x in self.__timer_tasks
if x["status"] == TimerTaskStatus.READY
or x["status"] == TimerTaskStatus.RUNNING
]
in_queue_count = len(in_queue_tasks)
if in_queue_count > 0:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !"
)
self.__timer_tasks = in_queue_tasks
self.timerTasksChanged.emit()
def checkTasks(
self
):
need_update = False
now = datetime.now()
for timer_task in self.__timer_tasks:
if timer_task["execute_time"] > now:
continue
if timer_task["status"] is not TimerTaskStatus.PENDING:
continue
if timer_task["execute_time"] <= now + timedelta(seconds = -5):
timer_task["status"] = TimerTaskStatus.OUTDATED
need_update = True
else:
timer_task["status"] = TimerTaskStatus.READY
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 onSortOrderToggleButtonClicked(
self
):
self.__sort_order = Qt.SortOrder.AscendingOrder\
if self.__sort_order is Qt.SortOrder.DescendingOrder\
else Qt.SortOrder.DescendingOrder
self.TimerTaskSortOrderToggleButton.setText(
"" if self.__sort_order is Qt.SortOrder.AscendingOrder else ""
)
self.updateTimerTaskList()
@Slot()
def onTimerTasksChanged(
self
):
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks))
self.updateTimerTaskList()
self.updateStat()
@Slot(dict)
def onTimerTaskIsRunning(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.RUNNING
self.timerTasksChanged.emit()
@Slot(dict)
def onTimerTaskIsExecuted(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.EXECUTED
self.timerTasksChanged.emit()
@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()
+358
View File
@@ -0,0 +1,358 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALTimerTaskWidget</class>
<widget class="QWidget" name="ALTimerTaskWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>定时任务 - AutoLibrary</string>
</property>
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout">
<property name="spacing">
<number>5</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>
<layout class="QHBoxLayout" name="TimerTaskStatusLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TotalTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>总任务:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="PendingTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #FF9800
}</string>
</property>
<property name="text">
<string>待执行:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="InQueueTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #2294FF
}</string>
</property>
<property name="text">
<string>队列中:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="ExecutedTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #4CAF50
}</string>
</property>
<property name="text">
<string>已执行:0</string>
</property>
</widget>
</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>
<widget class="QFrame" name="TimerTaskSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="TimerTaskSortLayout">
<property name="spacing">
<number>0</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>
<item>
<widget class="QPushButton" name="TimerTaskSortOrderToggleButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>↑</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="TimerTasksListWidget">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="TimerTaskEditLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="ClearAllTimerTasksButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>清除全部</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="AddTimerTaskButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加任务</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="TimerTaskEditSpaceFrame">
<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>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+150
View File
@@ -0,0 +1,150 @@
# -*- 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.sortByColumn(0, Qt.SortOrder.AscendingOrder)
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)
+1
View File
@@ -0,0 +1 @@
AL_VERSION = "1.0.0"
@@ -1,6 +1,6 @@
<RCC>
<qresource prefix="/res/icon">
<file>icons/AutoLibrary.ico</file>
<file>icons/AutoLibrary_32x32.ico</file>
</qresource>
<qresource prefix="/res/trans">
<file>translators/qtbase_zh_CN.qm</file>
+62
View File
@@ -0,0 +1,62 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
cd /d "%~dp0.."
echo [AutoLibrary complie] 检查翻译文件...
if exist translators (
cd translators
set ts_count=0
for %%f in (*.ts) do set /a ts_count+=1
if !ts_count! gtr 0 (
echo [AutoLibrary complie] 找到 !ts_count! 个 .ts 文件,开始编译翻译文件...
for %%f in (*.ts) do (
set "qm_filename=%%~nf.qm"
echo [AutoLibrary complie] 正在编译翻译文件: "%%f" -> "!qm_filename!"
pyside6-lrelease "%%f"
if !errorlevel! equ 0 (
echo [AutoLibrary complie] 翻译文件 "%%f" ✓ 编译成功,输出文件: "!qm_filename!"
) else (
echo [AutoLibrary complie] 翻译文件 "%%f" ✗ 编译失败
)
)
echo.
) else (
echo [AutoLibrary complie] 未找到任何 .ts 翻译文件
)
cd ..
) else (
echo [AutoLibrary complie] 未找到 translators 目录
)
echo.
set count=0
for %%f in (*.qrc) do set /a count+=1
if %count% equ 0 (
echo [AutoLibrary complie] 错误: 未找到任何 .qrc 文件
pause
exit /b 1
)
echo [AutoLibrary complie] 找到 %count% 个 .qrc 文件,开始编译...
echo.
for %%f in (*.qrc) do (
set "filename=%%~nf"
set "output_file=!filename!.py"
echo [AutoLibrary complie] 正在编译: "%%f" -> "!output_file!"
pyside6-rcc "%%f" -o "!output_file!"
if !errorlevel! equ 0 (
echo [AutoLibrary complie] 文件 "%%f" ✓ 编译成功,输出文件: "!output_file!"
) else (
echo [AutoLibrary complie] 文件 "%%f" ✗ 编译失败
)
echo.
)
echo [AutoLibrary complie] 所有操作完成。
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PARENT_DIR"
echo "[AutoLibrary complie] 检查翻译文件..."
if [ -d "translators" ]; then
cd translators
ts_files=(*.ts)
ts_count=${#ts_files[@]}
# 如果第一个元素是"*.ts"(表示没有匹配),则数量为0
if [ "$ts_count" -eq 1 ] && [ "${ts_files[0]}" = "*.ts" ]; then
ts_count=0
fi
if [ $ts_count -gt 0 ]; then
echo "[AutoLibrary complie] 找到 $ts_count 个 .ts 文件,开始编译翻译文件..."
for file in *.ts; do
base_name=$(basename "$file" .ts)
qm_file="${base_name}.qm"
echo "[AutoLibrary complie] 正在编译翻译文件: \"$file\" -> \"$qm_file\""
if pyside6-lrelease "$file"; then
echo "[AutoLibrary complie] 翻译文件 \"$file\" ✓ 编译成功,输出文件: \"$qm_file\""
else
echo "[AutoLibrary complie] 翻译文件 \"$file\" ✗ 编译失败"
fi
done
echo
else
echo "[AutoLibrary complie] 未找到任何 .ts 翻译文件"
fi
cd ..
else
echo "[AutoLibrary complie] 未找到 translators 目录"
fi
echo
file_count=$(ls *.qrc 2>/dev/null | wc -l)
if [ $file_count -eq 0 ]; then
echo "[AutoLibrary complie] 错误: 未找到任何 .qrc 文件"
exit 1
fi
echo "[AutoLibrary complie] 找到 $file_count 个 .qrc 文件,开始编译..."
echo
for file in *.qrc; do
base_name=$(basename "$file" .qrc)
output_file="${base_name}.py"
echo "[AutoLibrary complie] 正在编译: \"$file\" -> \"$output_file\""
if pyside6-rcc "$file" -o "$output_file"; then
echo "[AutoLibrary complie] 文件 \"$file\" ✓ 编译成功,输出文件: \"$output_file\""
else
echo "[AutoLibrary complie] 文件 \"$file\" ✗ 编译失败"
fi
echo
done
echo "[AutoLibrary complie] 所有操作完成。"
+33
View File
@@ -0,0 +1,33 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
cd /d "%~dp0.."
set count=0
for %%f in (*.ui) do set /a count+=1
if %count% equ 0 (
echo [AutoLibrary complie] 错误: 未找到任何 .ui 文件
pause
exit /b 1
)
echo [AutoLibrary complie] 找到 %count% 个 .ui 文件,开始编译...
echo.
for %%f in (*.ui) do (
set "filename=%%~nf"
set "output_file=Ui_!filename!.py"
echo [AutoLibrary complie] 正在编译: "%%f" -> "!output_file!"
pyside6-uic "%%f" -o "!output_file!"
if !errorlevel! equ 0 (
echo [AutoLibrary complie] 文件 "%%f" ✓ 编译成功,输出文件: "!output_file!"
) else (
echo [AutoLibrary complie] 文件 "%%f" ✗ 编译失败
)
echo.
)
echo [AutoLibrary complie] 所有操作完成。
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PARENT_DIR"
file_count=$(ls *.ui 2>/dev/null | wc -l)
if [ $file_count -eq 0 ]; then
echo "[AutoLibrary complie] 错误: 未找到任何 .ui 文件"
exit 1
fi
echo "[AutoLibrary complie] 找到 $file_count 个 .ui 文件,开始编译..."
echo
for file in *.ui; do
base_name=$(basename "$file" .ui)
output_file="Ui_${base_name}.py"
echo "[AutoLibrary complie] 正在编译: \"$file\" -> \"$output_file\""
if pyside6-uic "$file" -o "$output_file"; then
echo "[AutoLibrary complie] 文件 \"$file\" ✓ 编译成功,输出文件: \"$output_file\""
else
echo "[AutoLibrary complie] 文件 \"$file\" ✗ 编译失败"
fi
echo
done
echo "[AutoLibrary complie] 所有操作完成。"
+1
View File
@@ -0,0 +1 @@
this folder is used to store the batch scripts.

Before

Width:  |  Height:  |  Size: 785 KiB

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+71 -49
View File
@@ -16,14 +16,15 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.edge.service import Service
from MsgBase import MsgBase
from LibChecker import LibChecker
from LibLogin import LibLogin
from LibLogout import LibLogout
from LibReserve import LibReserve
from LibCheckin import LibCheckin
from base.MsgBase import MsgBase
from operators.LibChecker import LibChecker
from operators.LibLogin import LibLogin
from operators.LibLogout import LibLogout
from operators.LibReserve import LibReserve
from operators.LibCheckin import LibCheckin
from operators.LibRenew import LibRenew
from ConfigReader import ConfigReader
from utils.ConfigReader import ConfigReader
class AutoLib(MsgBase):
@@ -31,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():
raise Exception("浏览器驱动初始化失败")
else:
if not self.__initDriverUrl():
raise Exception("浏览器驱动URL初始化失败")
self.__initLibOperators()
def __initBrowserDriver(
@@ -47,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")
@@ -75,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
@@ -91,7 +103,7 @@ class AutoLib(MsgBase):
self.__driver = webdriver.Firefox(service=service, options=edge_options)
case _:
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
self.__driver.implicitly_wait(10)
self.__driver.implicitly_wait(1)
self.__driver.execute_script(
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
)
@@ -114,6 +126,7 @@ class AutoLib(MsgBase):
self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver)
self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver)
self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver)
self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver)
def __waitResponseLoad(
@@ -122,7 +135,7 @@ class AutoLib(MsgBase):
# wait for page load
try:
WebDriverWait(self.__driver, 5).until( # title contains "首页"
WebDriverWait(self.__driver, 2).until( # title contains "首页"
EC.title_contains("首页")
)
WebDriverWait(self.__driver, 2).until( # username field presence
@@ -147,7 +160,12 @@ class AutoLib(MsgBase):
self,
) -> bool:
self.__driver.get(self.__system_config_reader.get("library/host_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
return True
@@ -157,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,
@@ -183,77 +203,79 @@ class AutoLib(MsgBase):
# reserve
if run_mode["auto_reserve"]:
if self.__lib_checker.canReserve(reserve_info.get("date")):
if self.__lib_reserve.reserve(reserve_info):
self._showTrace(f"用户 {username} 预约成功 !")
if self.__lib_reserve.reserve(username, reserve_info):
result = 0
else:
self._showTrace(f"用户 {username} 预约失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法预约,已跳过")
result = 2
# checkin
if run_mode["auto_checkin"] and result == 2:
if self.__lib_checker.canCheckin(reserve_info.get("date")):
if self.__lib_checker.canCheckin():
if self.__lib_checkin.checkin(username):
self._showTrace(f"用户 {username} 签到成功 !")
result = 0
else:
self._showTrace(f"用户 {username} 签到失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法签到,已跳过")
result = 2
# renewal
if run_mode["auto_renewal"] and result == 2:
if self.__lib_checker.canRenew(reserve_info.get("date")):
pass
if record := self.__lib_checker.canRenew():
if self.__lib_renew.renew(username, record, reserve_info):
if self.__lib_checker.postRenewCheck(record):
result = 0
else:
result = 1
else:
result = 1
else:
self._showTrace(f"用户 {username} 无法续约,已跳过")
result = 2
# logout
if not self.__lib_logout.logout(
username,
username
):
# 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
+171 -99
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibChecker(LibOperator):
@@ -25,7 +25,7 @@ class LibChecker(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -44,9 +44,9 @@ class LibChecker(LibOperator):
seconds: float
) -> str:
hours = int(seconds // 3600)
minutes = int(seconds % 3600 // 60)
seconds = int(seconds % 60)
hours = int(seconds//3600)
minutes = int(seconds%3600//60)
seconds = int(seconds%60)
return f"{hours}{minutes}{seconds}"
@@ -55,7 +55,7 @@ class LibChecker(LibOperator):
) -> bool:
try:
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
).click()
WebDriverWait(self.__driver, 2).until(
@@ -67,6 +67,44 @@ class LibChecker(LibOperator):
return True
def __decodeReserveTime(
self,
time_element
) -> dict:
time_str = time_element.text.strip()
today = datetime.now().date()
if "明天" in time_str:
target_date = today + timedelta(days=1)
date = target_date.strftime("%Y-%m-%d")
elif "今天" in time_str:
target_date = today
date = target_date.strftime("%Y-%m-%d")
elif "昨天" in time_str:
target_date = today - timedelta(days=1)
date = target_date.strftime("%Y-%m-%d")
else:
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
if date_match:
date = date_match.group(1)
else:
date = ""
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
if time_match:
begin_time = time_match.group(1)
end_time = time_match.group(2)
else:
begin_time = ""
end_time = ""
return {
"date": date,
"time": {
"begin": begin_time,
"end": end_time
}
}
def __decodeReserveInfo(
self,
info_elements
@@ -108,42 +146,63 @@ class LibChecker(LibOperator):
By.CSS_SELECTOR, "a"
)
except:
return None
# process time element to get the date string
time_str = time_element.text.strip()
today = datetime.now().date()
if "明天" in time_str:
target_date = today + timedelta(days=1)
date_str = target_date.strftime("%Y-%m-%d")
elif "今天" in time_str:
target_date = today
date_str = target_date.strftime("%Y-%m-%d")
elif "昨天" in time_str:
target_date = today - timedelta(days=1)
date_str = target_date.strftime("%Y-%m-%d")
else:
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
if date_match:
date_str = date_match.group(1)
else:
date_str = ""
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
if time_match:
begin_time = time_match.group(1)
end_time = time_match.group(2)
else:
time_str = ""
return {
"date": "",
"time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""}
}
time = self.__decodeReserveTime(time_element)
info = self.__decodeReserveInfo(info_elements)
return {
"date": date_str,
"time": {
"begin": begin_time,
"end": end_time,
},
"date": time["date"],
"time": time["time"],
"info": info
}
def __loadReserveRecords(
self
) -> list:
try:
# check if there's any reservation on the date
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
)
reservations = self.__driver.find_elements(
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
)
return reservations
except:
self._showTrace("加载预约记录失败 !")
return None
def __showMoreReserveRecords(
self
) -> bool:
# load new reservations if still not sure
try:
WebDriverWait(self.__driver, 0.1).until(
EC.element_to_be_clickable((By.ID, "moreBtn"))
)
except:
# the reservation is the last one
return False
try:
more_btn = self.__driver.find_element(By.ID, "moreBtn")
if more_btn.is_displayed() and more_btn.is_enabled():
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
self.__driver.execute_script("arguments[0].click();", more_btn)
return True
else:
self._showTrace("用户无法加载更多预约记录")
return False
except:
self._showTrace("加载更多预约记录失败 !")
return False
def __getReserveRecord(
self,
wanted_date: str,
@@ -154,7 +213,6 @@ class LibChecker(LibOperator):
self._showTrace("日期未指定, 无法检查当前预约状态")
return None
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
date_obj = datetime.strptime(wanted_date, "%Y-%m-%d").date()
checked_count = 0
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
@@ -162,59 +220,34 @@ class LibChecker(LibOperator):
if not self.__navigateToReserveRecordPage():
return None
for _ in range(max_check_times):
try:
# check if there's any reservation on the date
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
)
reservations = self.__driver.find_elements(
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
)
except:
self._showTrace("加载预约记录失败 !")
reservations = self.__loadReserveRecords()
if reservations is None:
return None
for i in range(checked_count, len(reservations)): # the last one is load button
reservation = reservations[i]
for reservation in reservations[checked_count:]:
record = self.__decodeReserveRecord(reservation)
checked_count += 1
if record is None:
continue
record_date = record["date"]
record_time = record["time"]
status = record["info"]["status"]
location = record["info"]["location"]
if record_date == "" or record_time == {"begin": "", "end": ""}:
if record["date"] == "":
continue
is_wanted = (status == wanted_status)
# reservation is later than the given date, check the next one
if datetime.strptime(record_date, "%Y-%m-%d").date() > date_obj:
if record["time"] == {"begin": "", "end": ""}:
continue
# reservation is earlier than the given date, can reserve
if datetime.strptime(record_date, "%Y-%m-%d").date() < date_obj:
# record date is later than the given date, check the next one
if datetime.strptime(record["date"], "%Y-%m-%d").date() >\
datetime.strptime(wanted_date, "%Y-%m-%d").date():
continue
# record date is earlier than the given date, so there is no wanted record
if datetime.strptime(record["date"], "%Y-%m-%d").date() <\
datetime.strptime(wanted_date, "%Y-%m-%d").date():
return None
# query the wanted status
if is_wanted:
if record["info"]["status"] == wanted_status:
self._showTrace(
f"寻找到用户第 {i + 1} 条状态为 {wanted_status} 的预约记录, "
f"详细信息: {record_date} {record_time['begin']} - {record_time['end']} {location}"
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
f"详细信息: {record["date"]} "
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
)
return {
"index": i,
"date": record_date,
"time": record_time,
"status": wanted_status
}
checked_count = len(reservations)
# load new reservations if still not sure
try:
more_btn = self.__driver.find_element(By.ID, "moreBtn")
if more_btn.is_displayed() and more_btn.is_enabled():
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
self.__driver.execute_script("arguments[0].click();", more_btn)
else:
self._showTrace("用户无法加载更多预约记录")
break
except:
self._showTrace("加载更多预约记录失败 !")
return record
if not self.__showMoreReserveRecords():
break
return None
@@ -237,11 +270,11 @@ class LibChecker(LibOperator):
def canCheckin(
self,
date: str
self
) -> bool:
# have a reserved record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "已预约")
if record is not None:
begin_time = record["time"]["begin"]
@@ -252,21 +285,21 @@ class LibChecker(LibOperator):
if time_diff_seconds < -30*60:
self._showTrace(
f"用户在 {date} 的预约开始时间为 {begin_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
)
return False
# before in 30 minutes, can checkin
elif -30*60 <= time_diff_seconds < 0:
self._showTrace(
f"用户在 {date} 的预约开始时间为 {begin_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
)
return True
# past less than 30 minutes, can checkin
elif 0 <= time_diff_seconds < 30*60:
elif 0 <= time_diff_seconds < 30*60 - 5: # spare 5 seconds for the checkin process
self._showTrace(
f"用户在 {date} 的预约开始时间为 {begin_time}, "
f"当前时间已经 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
f"当前距离预约开始时间已经过去 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
)
return True
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
@@ -274,11 +307,11 @@ class LibChecker(LibOperator):
def canRenew(
self,
date: str
) -> bool:
self
):
# have a using record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "使用中")
if record is not None:
end_time = record["time"]["end"]
@@ -286,17 +319,56 @@ class LibChecker(LibOperator):
time_diff = end_time - datetime.now()
time_diff_seconds = time_diff.total_seconds()
# a using record is definitely after the begin time
trace_msg = (
f"用户在 {date} 的预约结束时间为 {end_time}, "
f"当前距离预约结束时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}"
)
if abs(time_diff_seconds) < 120*60:
self._showTrace(
f"用户在 {date} 的预约结束时间为 {end_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以续约"
self._showTrace(f"{trace_msg}, 可以续约")
return record
else:
self._showTrace(f"{trace_msg}, 无法续约")
return None
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return None
def postRenewCheck(
self,
record: dict
):
"""
Check if the renew operation is successful
Args:
record (dict): The expected record after renewal
Returns:
bool: True if the renew operation is successful, False otherwise
"""
# because the special circumstance that the renew operation
# do not show the success message or anything else,
# we need to check the record data to make sure the renew operation is successful.
# only check the given record date
date = record["date"]
act_record = self.__getReserveRecord(date, "使用中")
if act_record is not None:
if act_record["time"]["begin"] == record["time"]["begin"] and\
act_record["time"]["end"] == record["time"]["end"]:
self._showTrace(f"\n"\
f" 续约成功 !\n"\
f" 日 期 {date}\n"\
f" 时 间 {act_record["time"]["begin"]} - {act_record["time"]["end"]}\n"\
f" 位 置 {act_record["info"]["location"]}\n"
f" 状 态 {act_record["info"]["status"]}"
)
return True
else:
self._showTrace(
f"用户在 {date} 的预约结束时间为 {end_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法续约"
self._showTrace(f"\n"\
f" 续约失败 !\n"\
f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
)
return False
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果")
return False
+18 -9
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibCheckin(LibOperator):
@@ -25,7 +25,7 @@ class LibCheckin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -38,7 +38,7 @@ class LibCheckin(LibOperator):
) -> bool:
try:
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog"))
)
WebDriverWait(self.__driver, 2).until(
@@ -54,7 +54,6 @@ class LibCheckin(LibOperator):
except:
self._showTrace("签到时发生未知错误 !")
return False
print(result_message_element)
result_message = result_message_element.text
if "签到成功" in result_message:
try:
@@ -71,16 +70,21 @@ class LibCheckin(LibOperator):
f" {details[1]}\n"\
f" {details[2]}\n"\
f" {details[3]}\n"\
f" {details[4]}")
f" {details[4]}"
)
else:
self._showTrace(
self._showTrace(f"\n"\
" 签到成功 !\n"\
" 未获取到签到详情 !")
" 未获取到签到详情 !"
)
ok_btn.click()
return True
else:
failure_reason = result_message.replace("签到失败", "").strip()
self._showTrace(f"签到失败: {failure_reason}")
self._showTrace(f"\n"\
" 签到失败 !\n"\
f" {failure_reason}"
)
ok_btn.click()
return False
@@ -104,4 +108,9 @@ class LibCheckin(LibOperator):
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
return False
checkin_btn.click()
return self._waitResponseLoad()
if self._waitResponseLoad():
self._showTrace(f"用户 {username} 签到成功 !")
return True
else:
self._showTrace(f"用户 {username} 签到失败 !")
return False
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibCheckout(LibOperator):
@@ -25,7 +25,7 @@ class LibCheckout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+5 -5
View File
@@ -17,7 +17,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibLogin(LibOperator):
@@ -26,7 +26,7 @@ class LibLogin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -41,13 +41,13 @@ class LibLogin(LibOperator):
# wait to verify login success
try:
WebDriverWait(self.__driver, 5).until( # title contains "自选座位 :: 座位预约系统"
WebDriverWait(self.__driver, 2).until( # title contains "自选座位 :: 座位预约系统"
EC.title_contains("自选座位 :: 座位预约系统")
)
WebDriverWait(self.__driver, 3).until( # search button presence
WebDriverWait(self.__driver, 2).until( # search button presence
EC.presence_of_element_located((By.ID, "search"))
)
WebDriverWait(self.__driver, 3).until( # select content presence
WebDriverWait(self.__driver, 2).until( # select content presence
EC.presence_of_element_located((By.CLASS_NAME, "selectContent"))
)
return True
+2 -2
View File
@@ -13,7 +13,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibLogout(LibOperator):
@@ -22,7 +22,7 @@ class LibLogout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+216
View File
@@ -0,0 +1,216 @@
# -*- 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 datetime import datetime, timedelta
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from base.LibOperator import LibOperator
class LibRenew(LibOperator):
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver: any
):
super().__init__(input_queue, output_queue)
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
self.__driver.refresh()
return True
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __waitRenewDialog(
self
) -> bool:
try:
WebDriverWait(self.__driver, 2).until(
EC.visibility_of_element_located((By.ID, "extendDiv"))
)
head_message = WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv p.messageHead"))
)
result_message = WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage"))
)
except:
self._showTrace("续约时间选择界面加载失败 !")
return False
head_message = head_message.text.strip()
if "警告" in head_message:
result_message = result_message.text.strip()
self._showTrace(f"\n"\
f" 续约失败 !\n"\
f" {result_message}")
return False
try:
WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
)
)
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK"))
)
except:
self._showTrace("续约时间选择界面加载失败 !")
return False
return True
def __selectNearstTime(
self,
record: dict,
reserve_info: dict
) -> bool:
"""
TODO : this function is too long and too ugly
we need to refactor it to make it more readable.
but may be it is not a good idea to refactor it. :) who knows...
"""
end_time = record["time"]["end"]
renew_info = reserve_info["renew_time"]
max_diff = renew_info["max_diff"]
prefer_earlier = renew_info["prefer_early"]
target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60
renew_ok_btn = self.__driver.find_element(
By.CSS_SELECTOR, "#extendDiv .btnOK"
)
try:
renew_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR, "#extendDiv .renewal_List li"
)
free_times = []
best_time_diff = max_diff
best_actual_diff = None
best_time_opt = None
if not renew_time_opts:
self._showTrace("当前未查询到可用续约时间 !")
return False
for time_opt in renew_time_opts:
time_attr = time_opt.get_attribute("id")
if time_attr and time_attr.isdigit():
time_val = int(time_attr)
free_times.append(time_opt.text.strip())
else:
continue
actual_diff = time_val - target_renew_mins
abs_diff = abs(actual_diff)
if abs_diff < best_time_diff or (
abs_diff == best_time_diff and (
# 优先选择更早的时间
(prefer_earlier and actual_diff <= 0) or
# 优先选择更晚的时间
(not prefer_earlier and actual_diff >= 0)
)
):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
best_time_opt.click()
abs_time_diff = abs(best_actual_diff)
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于续约时间"
self._showTrace(
f"选择距离期望续约时间最近的 {best_time_opt.text}, "\
f"与期望续约时间相比 {time_relation}"
)
# update the actual renew end time
record["time"]["end"] = best_time_opt.text.strip()
renew_ok_btn.click()
return True
self._showTrace(
"无法选择最近的可用续约时间 !" \
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
)
self._showTrace(
f"当前可供续约的时间有: {free_times}"
)
return False
except:
self._showTrace("查询可用续约时间时发生未知错误 !")
return False
def renew(
self,
username: str,
record: dict,
reserve_info: dict
) -> bool:
if self.__driver is None:
self._showTrace("未提供有效 WebDriver 实例 !")
return False
try:
renew_btn = WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "btnExtend"))
)
except:
self._showTrace(f"用户 {username} 续约界面加载失败 !")
return False
if "disabled" in renew_btn.get_attribute("class"):
self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
return False
renew_btn.click()
if not self.__waitRenewDialog():
self._showTrace(f"用户 {username} 续约失败 !")
# After the renewal, the webpage will display a mask overlay,
# so we need to refresh the page for subsequent operations.
self.__driver.refresh()
return False
if not self.__selectNearstTime(record, reserve_info):
self._showTrace(f"用户 {username} 续约失败 !")
self.__driver.refresh()
return False
if self._waitResponseLoad():
return True
+133 -40
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibReserve(LibOperator):
@@ -25,7 +25,7 @@ class LibReserve(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -55,13 +55,13 @@ class LibReserve(LibOperator):
) -> bool:
try:
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CLASS_NAME, "layoutSeat"))
)
title_elements = []
# reserve failed without title elements, so we need to try
try:
WebDriverWait(self.__driver, 1).until(
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".layoutSeat dt"))
)
title_elements = self.__driver.find_elements(
@@ -88,11 +88,13 @@ class LibReserve(LibOperator):
f" {contents[1]}\n"\
f" {contents[2]}\n"\
f" {contents[3]}\n"\
f" 签到时间 {contents[5]}")
f" 签到时间 {contents[5]}"
)
else:
self._showTrace(f"\n"\
f" 预约成功 !\n"\
f" 未找获取到详细信息")
self._showTrace("\n"\
" 预约成功 !\n"\
" 未找获取到详细信息"
)
return True
except:
self._showTrace(f"预约结果加载失败 !")
@@ -187,12 +189,13 @@ class LibReserve(LibOperator):
reserve_info: dict
) -> bool:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
if reserve_info.get("satisfy_duration") is None:
reserve_info["satisfy_duration"] = True
self._showTrace("预约满足时长要求未指定, 默认满足")
if reserve_info["satisfy_duration"]:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
return True
@@ -234,7 +237,7 @@ class LibReserve(LibOperator):
# if end time is earlier than begin_time, exchange them
if end_mins < begin_mins:
self._showTrace(
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 自动交换"
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
)
reserve_info["end_time"] = begin_time
reserve_info["begin_time"] = end_time
@@ -261,10 +264,9 @@ class LibReserve(LibOperator):
if end_mins - begin_mins > 8*60:
self._showTrace(
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
f"{int((end_mins - begin_mins)/60)} 小时 "
f"{float((end_mins - begin_mins)/60)} 小时 "
f"超出最大时长 8 小时, 自动设置为 8 小时"
)
reserve_info["expect_duration"] = 8
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60)
return True
@@ -309,12 +311,12 @@ class LibReserve(LibOperator):
try:
# click the trigger element
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable(trigger_locator)
).click()
if option_locator:
# select the option element if specified
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable(option_locator)
).click()
self._showTrace(success_msg)
@@ -324,11 +326,52 @@ class LibReserve(LibOperator):
return False
def __clickElementByJS(
self,
trigger_locator_id: str,
option_query_selector: str,
fail_msg: str,
success_msg: str,
) -> bool:
script = f"""
try {{
var trigger = document.getElementById('{trigger_locator_id}');
if (trigger) {{
trigger.click();
var option = document.querySelector("{option_query_selector}");
if (option) {{
option.click();
return true;
}}
return false;
}}
return false;
}} catch (e) {{
return false;
}}
"""
result = self.__driver.execute_script(script)
time.sleep(0.1)
if result:
self._showTrace(success_msg)
else:
self._showTrace(fail_msg)
return result
def __selectDate(
self,
date_str: str
) -> bool:
if self.__clickElementByJS(
trigger_locator_id="onDate_select",
option_query_selector=f"p#options_onDate a[value='{date_str}']",
success_msg=f"日期 {date_str} 选择成功 !",
fail_msg=f"选择日期失败 ! : {date_str} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "onDate_select"),
option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"),
@@ -342,12 +385,20 @@ class LibReserve(LibOperator):
place: str
) -> bool:
actual_place = "1" if place == "图书馆" else "1"
place = "1" # the library only have this place :)
display_place = "图书馆"
if self.__clickElementByJS(
trigger_locator_id="display_building",
option_query_selector=f"p#options_building a[value='{place}']",
success_msg=f"预约场所 {display_place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "display_building"),
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{actual_place}']"),
success_msg=f"预约场所 {place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {place} 不可用"
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{place}']"),
success_msg=f"预约场所 {display_place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
)
@@ -357,6 +408,13 @@ class LibReserve(LibOperator):
) -> bool:
display_floor = self.__floor_map.get(floor)
if self.__clickElementByJS(
trigger_locator_id="floor_select",
option_query_selector=f"p#options_floor a[value='{floor}']",
success_msg=f"楼层 {display_floor} 选择成功 !",
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "floor_select"),
option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"),
@@ -371,12 +429,24 @@ class LibReserve(LibOperator):
) -> bool:
display_room = self.__room_map.get(room)
return self.__clickElement(
trigger_locator=(By.ID, f"room_{room}"),
option_locator=None,
success_msg=f"房间 {display_room} 选择成功 !",
fail_msg=f"选择房间失败 ! : {display_room} 不可用"
)
# find room
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "findRoom"))
).click()
except:
self._showTrace("加载房间/区域失败 !")
return False
# select room
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, f"room_{room}"))
).click()
self._showTrace(f"房间 {display_room} 选择成功 !")
return True
except:
self._showTrace(f"选择房间失败 ! : {display_room} 不可用")
return False
def __selectSeat(
@@ -386,9 +456,16 @@ class LibReserve(LibOperator):
try:
# wait fot seat layout element to load
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.ID, "seatLayout"))
)
WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']"))
)
except:
self._showTrace(f"座位加载失败 !")
return False
try:
all_seats = self.__driver.find_elements(
By.CSS_SELECTOR, "li[id^='seat_']"
)
@@ -397,7 +474,7 @@ class LibReserve(LibOperator):
if not seat_id_upper == seat.text.lstrip('0'):
continue
seat_link = seat.find_element(By.TAG_NAME, "a")
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable(seat_link)
)
seat_link.click()
@@ -419,6 +496,15 @@ class LibReserve(LibOperator):
prefer_earlier: bool = True
) -> int:
try:
WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, f"#{time_id} ul li a")
)
)
except:
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
return -1
try:
all_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR,
@@ -429,6 +515,9 @@ class LibReserve(LibOperator):
best_actual_diff = None
best_time_opt = None
if not all_time_opts:
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
return -1
for time_opt in all_time_opts:
time_attr = time_opt.get_attribute("time")
if time_attr == "now":
@@ -490,6 +579,7 @@ class LibReserve(LibOperator):
expect_begin_time = actual_begin_time = begin_time["time"]
expect_end_time = actual_end_time = end_time["time"]
expect_begin_mins = self.__timeToMins(expect_begin_time)
actual_begin_mins = expect_begin_mins
expect_end_mins = self.__timeToMins(expect_end_time)
# select the begin time
@@ -503,11 +593,18 @@ class LibReserve(LibOperator):
return False
else:
actual_begin_time = self.__minsToTime(expect_begin_mins)
actual_begin_mins = self.__timeToMins(actual_begin_time)
# if 'satisfy_duration' is True.
# select the end time based on the begin time
# (because it may be changed under the 'max time diff' strategy) and expect duration.
if satisfy_duration:
expect_end_mins = int(expect_begin_mins + expct_duration*60)
expect_end_mins = int(actual_begin_mins + expct_duration*60)
if expect_end_mins > self.__timeToMins("23:30"):
expect_end_mins = self.__timeToMins("23:30")
self._showTrace(
f"预约持续时间 {expct_duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
)
expect_end_time = self.__minsToTime(expect_end_mins)
self._showTrace(
f"需要满足期望预约持续时间: {expct_duration} 小时, "\
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
@@ -532,6 +629,7 @@ class LibReserve(LibOperator):
def reserve(
self,
username: str,
reserve_info: dict
) -> bool:
@@ -544,7 +642,7 @@ class LibReserve(LibOperator):
return False
# map page
try:
WebDriverWait(self.__driver, 5).until(
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
).click()
WebDriverWait(self.__driver, 2).until(
@@ -553,22 +651,13 @@ class LibReserve(LibOperator):
except:
self._showTrace(f"加载预约选座页面失败 !")
return False
# date, place, floor
# date, place, floor, room
if not self.__selectDate(reserve_info["date"]):
return False
if not self.__selectPlace(reserve_info["place"]):
return False
if not self.__selectFloor(reserve_info["floor"]):
return False
# room find
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "findRoom"))
).click()
except:
self._showTrace("加载房间/区域失败 !")
return False
# room
if not self.__selectRoom(reserve_info["room"]):
return False
else:
@@ -596,4 +685,8 @@ class LibReserve(LibOperator):
self._showTrace(f"预约提交失败 !")
if not submit_reserve and have_hover_on_page:
self.__driver.refresh()
if reserve_success:
self._showTrace(f"用户 {username} 预约成功 !")
else:
self._showTrace(f"用户 {username} 预约失败 !")
return reserve_success
+12
View File
@@ -0,0 +1,12 @@
"""
Operators module for the AutoLibrary project.
Here are the classes and modules in this package:
- AutoLib: AutoLibrary operator.
- LibLogin: Library operator for logging in.
- LibLogout: Library operator for logging out.
- LibReserve: Library operator for reserving seat.
- LibCheckin: Library operator for checking in seat.
- LibCheckout: Library operator for checking out seat.
- LibRenew: Library operator for renewing seat.
"""
+7
View File
@@ -0,0 +1,7 @@
"""
Utils module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigReader: Configuration reader class for the AutoLibrary project.
- ConfigWriter: Configuration writer class for the AutoLibrary project.
"""