mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 07:23:03 +08:00
chore(*): refactor the project structure
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
# -*- 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
|
||||
|
||||
from PySide6.QtCore import QTranslator
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from gui.ALMainWindow import ALMainWindow
|
||||
from gui import AutoLibraryResource
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
translator = QTranslator()
|
||||
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
|
||||
app.installTranslator(translator)
|
||||
app.setStyle('Fusion')
|
||||
window = ALMainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- 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 queue
|
||||
|
||||
from base.MsgBase import MsgBase
|
||||
|
||||
|
||||
class LibOperator(MsgBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- 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 time
|
||||
import queue
|
||||
|
||||
|
||||
class MsgBase:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
self._class_name = self.__class__.__name__
|
||||
self._input_queue = input_queue
|
||||
self._output_queue = output_queue
|
||||
|
||||
|
||||
def _showMsg(
|
||||
self,
|
||||
msg: str
|
||||
):
|
||||
|
||||
self._output_queue.put(f"[{self._class_name:<12}] >>> : {msg}")
|
||||
|
||||
|
||||
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:<12}] : {msg}")
|
||||
|
||||
|
||||
def _waitMsg(
|
||||
self,
|
||||
timeout: float = 1.0
|
||||
) -> str:
|
||||
|
||||
try:
|
||||
msg = self._input_queue.get(timeout=timeout)
|
||||
return msg
|
||||
except queue.Empty:
|
||||
return None
|
||||
|
||||
|
||||
def _inputMsg(
|
||||
self,
|
||||
timeout: float = 1.0
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
self._input_queue.get(timeout=timeout)
|
||||
return True
|
||||
except queue.Empty:
|
||||
return False
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -0,0 +1,866 @@
|
||||
# -*- 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 gui.Ui_ALConfigWidget import Ui_ALConfigWidget
|
||||
from gui.SeatMapWidget import SeatMapWidget
|
||||
|
||||
from gui.SeatMapTable import seats_maps
|
||||
from utils.ConfigReader import ConfigReader
|
||||
from utils.ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
configWidgetCloseSingal = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
config_paths = {
|
||||
"system": "",
|
||||
"users": ""
|
||||
}
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.__config_paths = config_paths
|
||||
self.__config_data = {"system": {}, "users": {}}
|
||||
self.__seat_map_widget = None
|
||||
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initlizeDefaultConfigPaths()
|
||||
if not self.initlizeConfigs():
|
||||
self.close()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initilizeUserInfoWidget()
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
|
||||
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
|
||||
self.SelectSeatsButton.clicked.connect(self.onSelectSeatsButtonClicked)
|
||||
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
|
||||
):
|
||||
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
self.__default_config_paths = {
|
||||
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
|
||||
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json"))
|
||||
}
|
||||
|
||||
|
||||
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 initlizeConfig(
|
||||
self,
|
||||
which: str
|
||||
) -> bool:
|
||||
|
||||
msg = ""
|
||||
is_success = True
|
||||
if which == "system":
|
||||
system_config_path = self.__config_paths[which]
|
||||
if not os.path.exists(system_config_path):
|
||||
self.__config_data[which] = self.defaultSystemConfig()
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if self.saveSystemConfig(self.__config_paths[which], self.__config_data[which]):
|
||||
msg += f"系统配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||
else:
|
||||
is_success = False
|
||||
else:
|
||||
self.__config_data[which] = self.loadSystemConfig(system_config_path)
|
||||
if self.__config_data[which] is None:
|
||||
is_success = False
|
||||
elif which == "users":
|
||||
users_config_path = self.__config_paths[which]
|
||||
if not os.path.exists(users_config_path):
|
||||
self.__config_data[which] = self.defaultUsersConfig()
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if self.saveUsersConfig(self.__config_paths[which], self.__config_data[which]):
|
||||
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||
else:
|
||||
is_success = False
|
||||
else:
|
||||
self.__config_data[which] = self.loadUsersConfig(users_config_path)
|
||||
if self.__config_data[which] is None:
|
||||
is_success = False
|
||||
if msg:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
f"配置文件初始化完成: \n{msg}"
|
||||
)
|
||||
return is_success
|
||||
|
||||
|
||||
def initlizeConfigs(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
is_success = True
|
||||
for which in ["system", "users"]:
|
||||
if not self.__config_paths[which]:
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if not self.initlizeConfig(which):
|
||||
is_success = False
|
||||
break
|
||||
self.initlizeConfigToWidget(which, self.__config_data[which])
|
||||
return is_success
|
||||
|
||||
|
||||
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.EchoMode.Password)
|
||||
self.ShowPasswordCheckBox.setChecked(False)
|
||||
self.FloorComboBox.setCurrentIndex(0)
|
||||
self.onFloorComboBoxCurrentIndexChanged()
|
||||
self.DateEdit.setDate(QDate.currentDate())
|
||||
self.DateEdit.setMinimumDate(QDate.currentDate())
|
||||
self.BeginTimeEdit.setTime(QTime.currentTime())
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(False)
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(30)
|
||||
self.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
|
||||
self.PreferLateEndTimeCheckBox.setChecked(False)
|
||||
self.MaxEndTimeDiffSpinBox.setValue(30)
|
||||
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.__config_data["users"] = self.defaultUsersConfig()
|
||||
for index in range(self.UserListWidget.count()):
|
||||
user_config = self.collectUserConfigFromUserListWidget(index)
|
||||
if user_config:
|
||||
self.__config_data["users"]["users"].append(user_config)
|
||||
if not self.saveUsersConfig(
|
||||
users_config_path,
|
||||
self.__config_data["users"]
|
||||
):
|
||||
return False
|
||||
if system_config_path:
|
||||
self.__config_data["system"] = self.collectSystemConfigFromWidget()
|
||||
if not self.saveSystemConfig(
|
||||
system_config_path,
|
||||
self.__config_data["system"]
|
||||
):
|
||||
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.__config_data["system"].update(system_config)
|
||||
self.setSystemConfigToWidget(self.__config_data["system"])
|
||||
return True
|
||||
if users_config is not None:
|
||||
self.__config_data["users"].update(users_config)
|
||||
self.fillUsersList(self.__config_data["users"])
|
||||
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": 30,
|
||||
"prefer_early": False
|
||||
},
|
||||
"end_time": {
|
||||
"time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
|
||||
"max_diff": 30,
|
||||
"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 onSeatMapWidgetClosed(
|
||||
self,
|
||||
selected_seats: list[str]
|
||||
):
|
||||
|
||||
self.__seat_map_widget.seatMapWidgetClosed.disconnect(self.onSeatMapWidgetClosed)
|
||||
self.__seat_map_widget.deleteLater()
|
||||
self.__seat_map_widget = None
|
||||
if len(selected_seats) == 0:
|
||||
return
|
||||
self.SeatIDEdit.setText(",".join(selected_seats))
|
||||
|
||||
@Slot()
|
||||
def onSelectSeatsButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
floor = self.FloorComboBox.currentText()
|
||||
room = self.RoomComboBox.currentText()
|
||||
floor_idx = self.__floor_rmap[floor]
|
||||
room_idx = self.__room_rmap[room]
|
||||
if self.__seat_map_widget is None:
|
||||
self.__seat_map_widget = SeatMapWidget(
|
||||
self,
|
||||
floor,
|
||||
room,
|
||||
seats_maps[floor_idx][room_idx]
|
||||
)
|
||||
self.__seat_map_widget.seatMapWidgetClosed.connect(self.onSeatMapWidgetClosed)
|
||||
self.__seat_map_widget.show()
|
||||
self.__seat_map_widget.raise_()
|
||||
self.__seat_map_widget.activateWindow()
|
||||
self.__seat_map_widget.selectSeats(self.SeatIDEdit.text().split(","))
|
||||
|
||||
@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.BrowseBrowserDriverEdit.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, ""
|
||||
):
|
||||
msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
|
||||
else:
|
||||
msg += f"系统配置文件导出失败: \n'{system_config_path}'\n"
|
||||
if users_config_path:
|
||||
if self.saveConfigs(
|
||||
"", users_config_path
|
||||
):
|
||||
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
|
||||
else:
|
||||
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.__config_data["system"] = self.defaultSystemConfig()
|
||||
self.__config_data["users"] = self.defaultUsersConfig()
|
||||
self.__config_paths = {
|
||||
"system": system_config_path,
|
||||
"users": users_config_path
|
||||
}
|
||||
self.initlizeConfigToWidget("system", self.__config_data["system"])
|
||||
self.initlizeConfigToWidget("users", self.__config_data["users"])
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.UserListWidget.currentItem() is not None:
|
||||
user_config = self.collectUserConfigFromUserInfoWidget()
|
||||
if user_config:
|
||||
self.UserListWidget.currentItem().setData(Qt.UserRole, user_config)
|
||||
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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,317 @@
|
||||
# -*- 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 operators.AutoLib import AutoLib
|
||||
from utils.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()
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
self.__config_paths = {
|
||||
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json")),
|
||||
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
|
||||
}
|
||||
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()
|
||||
@@ -0,0 +1,270 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALMainWindow</class>
|
||||
<widget class="QMainWindow" name="ALMainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>540</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>540</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>AutoLibrary</string>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="CentralWidget">
|
||||
<layout class="QVBoxLayout" name="CentralWidgetLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ControlLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="ControlSpaceFrame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1280</width>
|
||||
<height>0</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="QPushButton" name="ConfigButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>配置</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="StopButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>停止脚本</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="StartButton">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>开始运行自动脚本</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">background-color: rgb(10, 170, 10);
|
||||
font: 12pt "Microsoft YaHei UI";
|
||||
color: rgb(255, 255, 255);
|
||||
font: 9pt "Segoe UI";
|
||||
font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>启动脚本</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="MessageIOTextEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>175</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QMenu::icon {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="midLineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="tabChangesFocus">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="undoRedoEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="lineWrapMode">
|
||||
<enum>QPlainTextEdit::LineWrapMode::NoWrap</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextInteractionFlag::LinksAccessibleByKeyboard|Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextBrowserInteraction|Qt::TextInteractionFlag::TextEditable|Qt::TextInteractionFlag::TextEditorInteraction|Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
|
||||
</property>
|
||||
<property name="backgroundVisible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="centerOnScroll">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="MessageLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="MessageEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="SendButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>发送</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="MenuBar">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>540</width>
|
||||
<height>33</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="nativeMenuBar">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="StatusBar">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,8 @@
|
||||
<RCC>
|
||||
<qresource prefix="/res/icon">
|
||||
<file>icons/AutoLibrary.ico</file>
|
||||
</qresource>
|
||||
<qresource prefix="/res/trans">
|
||||
<file>translators/qtbase_zh_CN.qm</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -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 SeatFrame(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;
|
||||
}
|
||||
""")
|
||||
@@ -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
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
# -*- 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.SeatFrame import SeatFrame
|
||||
|
||||
|
||||
class SeatMapWidget(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 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 = SeatFrame(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()
|
||||
@@ -0,0 +1 @@
|
||||
this folder is used to store the config files.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 785 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,283 @@
|
||||
# -*- 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 selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.edge.service import Service
|
||||
|
||||
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 utils.ConfigReader import ConfigReader
|
||||
|
||||
|
||||
class AutoLib(MsgBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__system_config_reader = None
|
||||
self.__users_config_reader = None
|
||||
self.__driver = None
|
||||
|
||||
|
||||
def __initBrowserDriver(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
self._showTrace("正在初始化浏览器驱动......")
|
||||
edge_options = webdriver.EdgeOptions()
|
||||
|
||||
if self.__system_config_reader.get("web_driver/headless"):
|
||||
edge_options.add_argument("--headless")
|
||||
edge_options.add_argument("--disable-gpu")
|
||||
edge_options.add_argument("--no-sandbox")
|
||||
edge_options.add_argument("--disable-dev-shm-usage")
|
||||
|
||||
# must be 1920x1080, otherwise the page will cause some elements not accessible
|
||||
edge_options.add_argument("--window-size=1920,1080")
|
||||
edge_options.add_argument("--remote-allow-origins=*")
|
||||
|
||||
# omit ssl errors and verbose log level
|
||||
edge_options.add_argument("--ignore-certificate-errors")
|
||||
edge_options.add_argument("--ignore-ssl-errors")
|
||||
edge_options.add_argument("--log-level=OFF")
|
||||
edge_options.add_argument("--silent")
|
||||
|
||||
edge_options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
edge_options.add_experimental_option("useAutomationExtension", False)
|
||||
edge_options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
edge_options.add_argument(
|
||||
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "\
|
||||
"Chrome/120.0.0.0 "\
|
||||
"Safari/537.36 "\
|
||||
"Edg/120.0.0.0"
|
||||
)
|
||||
|
||||
# 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 = os.path.abspath(self.__driver_path)
|
||||
try:
|
||||
service = None
|
||||
if self.__driver_path:
|
||||
service = Service(executable_path=self.__driver_path)
|
||||
match self.__driver_type.lower():
|
||||
case "edge":
|
||||
self.__driver = webdriver.Edge(service=service, options=edge_options)
|
||||
case "chrome":
|
||||
self.__driver = webdriver.Chrome(service=service, options=edge_options)
|
||||
case "firefox":
|
||||
self.__driver = webdriver.Firefox(service=service, options=edge_options)
|
||||
case _:
|
||||
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
|
||||
self.__driver.implicitly_wait(1)
|
||||
self.__driver.execute_script(
|
||||
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
)
|
||||
except Exception as e:
|
||||
self._showTrace(f"浏览器驱动初始化失败: {e}")
|
||||
return False
|
||||
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
|
||||
return True
|
||||
|
||||
|
||||
def __initLibOperators(
|
||||
self
|
||||
):
|
||||
|
||||
if not self.__driver:
|
||||
self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !")
|
||||
return
|
||||
self.__lib_checker = LibChecker(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
|
||||
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)
|
||||
|
||||
|
||||
def __waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# wait for page load
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until( # title contains "首页"
|
||||
EC.title_contains("首页")
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # username field presence
|
||||
EC.presence_of_element_located((By.NAME, "username"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # password field presence
|
||||
EC.presence_of_element_located((By.NAME, "password"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # captcha field presence
|
||||
EC.presence_of_element_located((By.NAME, "answer"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # captcha image presence
|
||||
EC.presence_of_element_located((By.ID, "loadImgId"))
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 !")
|
||||
return False
|
||||
|
||||
|
||||
def __initDriverUrl(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
url = self.__system_config_reader.get("library/host_url")
|
||||
url += self.__system_config_reader.get("library/login_url")
|
||||
self.__driver.get(url)
|
||||
if not self.__waitResponseLoad():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __run(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
reserve_info: dict
|
||||
) -> int:
|
||||
|
||||
# result : 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),
|
||||
):
|
||||
return 1
|
||||
"""
|
||||
Here, we collect the run mode from the config file.
|
||||
"""
|
||||
run_mode = self.__system_config_reader.get("mode/run_mode", 0)
|
||||
run_mode = {
|
||||
"auto_reserve": run_mode&0x1,
|
||||
"auto_checkin": run_mode&0x2,
|
||||
"auto_renewal": run_mode&0x4,
|
||||
}
|
||||
# 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} 预约成功 !")
|
||||
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_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
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法续约,已跳过")
|
||||
result = 2
|
||||
# logout
|
||||
if not self.__lib_logout.logout(
|
||||
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
|
||||
return result
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
system_config_reader: ConfigReader,
|
||||
users_config_reader: ConfigReader
|
||||
):
|
||||
|
||||
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()
|
||||
|
||||
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()}"
|
||||
)
|
||||
for user in users:
|
||||
user_counter["current"] += 1
|
||||
self._showTrace(
|
||||
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......"
|
||||
)
|
||||
r = self.__run(
|
||||
username=user["username"],
|
||||
password=user["password"],
|
||||
reserve_info=user["reserve_info"],
|
||||
)
|
||||
if r == 0:
|
||||
user_counter["success"] += 1
|
||||
elif r == 1:
|
||||
user_counter["failed"] += 1
|
||||
elif r == 2:
|
||||
user_counter["passed"] += 1
|
||||
self._showTrace(f"处理完成, 共计 {user_counter["current"]} 个用户, "\
|
||||
f"成功 {user_counter["success"]} 个用户, "\
|
||||
f"失败 {user_counter["failed"]} 个用户, "\
|
||||
f"跳过 {user_counter["passed"]} 个用户"
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def close(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
if self.__driver:
|
||||
self.__driver.quit()
|
||||
self.__driver = None
|
||||
self._showTrace(f"浏览器驱动已关闭")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"浏览器驱动未初始化, 无需关闭")
|
||||
return False
|
||||
@@ -0,0 +1,333 @@
|
||||
# -*- 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 re
|
||||
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 LibChecker(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
|
||||
|
||||
@staticmethod
|
||||
def __formatDiffTime(
|
||||
seconds: float
|
||||
) -> str:
|
||||
|
||||
hours = int(seconds//3600)
|
||||
minutes = int(seconds%3600//60)
|
||||
seconds = int(seconds%60)
|
||||
return f"{hours} 时 {minutes} 分 {seconds} 秒"
|
||||
|
||||
|
||||
def __navigateToReserveRecordPage(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
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
|
||||
) -> str:
|
||||
|
||||
location = ""
|
||||
status = ""
|
||||
for info in info_elements:
|
||||
if "已预约" in info.text:
|
||||
status = "已预约"
|
||||
elif "使用中" in info.text:
|
||||
status = "使用中"
|
||||
elif "已完成" in info.text:
|
||||
status = "已完成"
|
||||
elif "已结束使用" in info.text:
|
||||
status = "已结束使用"
|
||||
elif "已取消" in info.text:
|
||||
status = "已取消"
|
||||
elif "失约" in info.text:
|
||||
status = "失约"
|
||||
elif "图书馆" in info.text:
|
||||
location = info.text.strip()
|
||||
return {
|
||||
"location": location,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveRecord(
|
||||
self,
|
||||
reservation
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
info_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
except:
|
||||
return {
|
||||
"date": "",
|
||||
"time": {"begin": "", "end": ""},
|
||||
"info": {"location": "", "status": ""}
|
||||
}
|
||||
time = self.__decodeReserveTime(time_element)
|
||||
info = self.__decodeReserveInfo(info_elements)
|
||||
return {
|
||||
"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,
|
||||
wanted_status: str
|
||||
) -> dict:
|
||||
|
||||
if wanted_date is None:
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态")
|
||||
return None
|
||||
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
|
||||
|
||||
checked_count = 0
|
||||
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
|
||||
|
||||
if not self.__navigateToReserveRecordPage():
|
||||
return None
|
||||
for _ in range(max_check_times):
|
||||
reservations = self.__loadReserveRecords()
|
||||
if reservations is None:
|
||||
return None
|
||||
for reservation in reservations[checked_count:]:
|
||||
record = self.__decodeReserveRecord(reservation)
|
||||
checked_count += 1
|
||||
if record is None:
|
||||
continue
|
||||
if record["date"] == "":
|
||||
continue
|
||||
if record["time"] == {"begin": "", "end": ""}:
|
||||
continue
|
||||
# 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
|
||||
if record["info"]["status"] == wanted_status:
|
||||
self._showTrace(
|
||||
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
|
||||
f"详细信息: {record["date"]} "
|
||||
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
|
||||
)
|
||||
return record
|
||||
if not self.__showMoreReserveRecords():
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# no reserved or using record in the given date
|
||||
# then can reserve
|
||||
if self.__getReserveRecord(date, "已预约") is None:
|
||||
if self.__getReserveRecord(date, "使用中") is None:
|
||||
self._showTrace(f"用户在 {date} 可以预约")
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约")
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
|
||||
|
||||
def canCheckin(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# have a reserved record in the given date
|
||||
record = self.__getReserveRecord(date, "已预约")
|
||||
if record is not None:
|
||||
begin_time = record["time"]["begin"]
|
||||
begin_time = datetime.strptime(f"{date} {begin_time}", "%Y-%m-%d %H:%M")
|
||||
time_diff = datetime.now() - begin_time
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# before 30 minutes, cant checkin
|
||||
if time_diff_seconds < -30*60:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
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))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
# past less than 30 minutes, can checkin
|
||||
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))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
|
||||
return False
|
||||
|
||||
|
||||
def canRenew(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# have a using record in the given date
|
||||
record = self.__getReserveRecord(date, "使用中")
|
||||
if record is not None:
|
||||
end_time = record["time"]["end"]
|
||||
end_time = datetime.strptime(f"{date} {end_time}", "%Y-%m-%d %H:%M")
|
||||
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"{trace_msg}, 可以续约")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"{trace_msg}, 无法续约")
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
|
||||
return False
|
||||
@@ -0,0 +1,107 @@
|
||||
# -*- 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 re
|
||||
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 LibCheckin(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:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "resultMessage"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.CLASS_NAME, "btnOK"))
|
||||
)
|
||||
result_message_element = self.__driver.find_element(
|
||||
By.CLASS_NAME, "resultMessage"
|
||||
)
|
||||
ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK")
|
||||
except:
|
||||
self._showTrace("签到时发生未知错误 !")
|
||||
return False
|
||||
print(result_message_element)
|
||||
result_message = result_message_element.text
|
||||
if "签到成功" in result_message:
|
||||
try:
|
||||
detail_elements = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".resultMessage dd"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
if detail_elements:
|
||||
details = [element.text for element in detail_elements if element.text.strip()]
|
||||
if len(details) >= 5:
|
||||
self._showTrace(f"\n"\
|
||||
f" 签到成功 !\n"\
|
||||
f" {details[1]}\n"\
|
||||
f" {details[2]}\n"\
|
||||
f" {details[3]}\n"\
|
||||
f" {details[4]}")
|
||||
else:
|
||||
self._showTrace(
|
||||
" 签到成功 !\n"\
|
||||
" 未获取到签到详情 !")
|
||||
ok_btn.click()
|
||||
return True
|
||||
else:
|
||||
failure_reason = result_message.replace("签到失败", "").strip()
|
||||
self._showTrace(f"签到失败: {failure_reason}")
|
||||
ok_btn.click()
|
||||
return False
|
||||
|
||||
|
||||
def checkin(
|
||||
self,
|
||||
username: str
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
return False
|
||||
try:
|
||||
checkin_btn = WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "btnCheckIn"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"用户 {username} 签到界面加载失败 !")
|
||||
return False
|
||||
if "disabled" in checkin_btn.get_attribute("class"):
|
||||
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
|
||||
return False
|
||||
checkin_btn.click()
|
||||
return self._waitResponseLoad()
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- 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 re
|
||||
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 LibCheckout(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
|
||||
@@ -0,0 +1,210 @@
|
||||
# -*- 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 time
|
||||
import queue
|
||||
import base64
|
||||
|
||||
import ddddocr
|
||||
|
||||
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 LibLogin(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
self.__ddddocr = ddddocr.DdddOcr()
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# wait to verify login success
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until( # title contains "自选座位 :: 座位预约系统"
|
||||
EC.title_contains("自选座位 :: 座位预约系统")
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # search button presence
|
||||
EC.presence_of_element_located((By.ID, "search"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # select content presence
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "selectContent"))
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准")
|
||||
return False
|
||||
|
||||
|
||||
def __fillLogInElements(
|
||||
self,
|
||||
username: str,
|
||||
password: str
|
||||
) -> bool:
|
||||
|
||||
# ensure elements presence and fill them
|
||||
try:
|
||||
username_element = self.__driver.find_element(By.NAME, "username")
|
||||
username_element.clear()
|
||||
username_element.send_keys(username)
|
||||
password_element = self.__driver.find_element(By.NAME, "password")
|
||||
password_element.clear()
|
||||
password_element.send_keys(password)
|
||||
except Exception as e:
|
||||
self._showTrace(f"用户名或密码填写失败 ! : {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __autoRecognizeCaptcha(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# auto recognize captcha
|
||||
try:
|
||||
captcha_img = self.__driver.find_element(By.ID, "loadImgId")
|
||||
img_src = captcha_img.get_attribute("src")
|
||||
base64_str = img_src.split(',', 1)[1]
|
||||
captcha_img = base64.b64decode(base64_str)
|
||||
captcha_text = self.__ddddocr.classification(captcha_img)
|
||||
captcha_text = ''.join(filter(str.isalnum, captcha_text)).lower()
|
||||
self._showTrace(f"识别到验证码为 : '{captcha_text}'.")
|
||||
if len(captcha_text) != 4:
|
||||
raise Exception("识别到的验证码长度不等于 4 个字符 !")
|
||||
return captcha_text
|
||||
except Exception as e:
|
||||
self._showTrace(f"验证码识别失败 ! : {e}")
|
||||
self.__refreshCaptcha()
|
||||
return ""
|
||||
|
||||
|
||||
def __manualRecognizeCaptcha(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# manual recognize captcha
|
||||
try:
|
||||
self._show_msg("请输入验证码:")
|
||||
captcha_text = self._wait_msg(timeout=15)
|
||||
self._showTrace(f"输入的验证码为 : '{captcha_text}'.")
|
||||
if len(captcha_text) != 4:
|
||||
raise Exception("输入的验证码长度不等于 4 个字符 !")
|
||||
return captcha_text
|
||||
except Exception as e:
|
||||
self._showTrace(f"输入验证码失败 ! : {e}")
|
||||
self.__refreshCaptcha()
|
||||
return ""
|
||||
|
||||
|
||||
def __refreshCaptcha(
|
||||
self
|
||||
):
|
||||
|
||||
# refresh captcha
|
||||
try:
|
||||
self._showTrace("刷新验证码......")
|
||||
self.__driver.find_element(
|
||||
By.ID, "loadImgId"
|
||||
).click()
|
||||
time.sleep(1)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"刷新验证码失败 ! : {e}")
|
||||
self.__refreshCaptcha()
|
||||
return False
|
||||
|
||||
|
||||
def __solveCaptcha(
|
||||
self,
|
||||
auto_captcha: bool = True
|
||||
) -> str:
|
||||
|
||||
max_attempts = 5
|
||||
|
||||
for _ in range(max_attempts):
|
||||
if auto_captcha:
|
||||
captcha_text = self.__autoRecognizeCaptcha()
|
||||
else:
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
|
||||
captcha_text = self.__manualRecognizeCaptcha()
|
||||
if captcha_text:
|
||||
return captcha_text
|
||||
self._showTrace(f"验证码识别失败 {max_attempts} 次, 请检查验证码是否正确 !")
|
||||
return ""
|
||||
|
||||
|
||||
def __fillCaptchaElement(
|
||||
self,
|
||||
captcha_text: str
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
captcha_element = self.__driver.find_element(By.NAME, "answer")
|
||||
captcha_element.clear()
|
||||
captcha_element.send_keys(captcha_text)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"验证码填写失败 ! : {e}")
|
||||
self.__refreshCaptcha()
|
||||
return False
|
||||
|
||||
|
||||
def login(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
max_attempts: int = 5,
|
||||
auto_captcha: bool = True
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
return False
|
||||
# begin login process
|
||||
for attempt in range(max_attempts):
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......")
|
||||
if not self.__fillLogInElements(
|
||||
username,
|
||||
password,
|
||||
):
|
||||
continue
|
||||
captcha_text = self.__solveCaptcha(auto_captcha)
|
||||
if not captcha_text:
|
||||
continue
|
||||
if not self.__fillCaptchaElement(captcha_text):
|
||||
continue
|
||||
self._showTrace("尝试登录...")
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
By.XPATH,
|
||||
"//input[@type='button' and @value='登录']"
|
||||
).click()
|
||||
except Exception as e:
|
||||
self._showTrace(f"登录失败 ! : {e}")
|
||||
continue
|
||||
if self._waitResponseLoad():
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !")
|
||||
return False
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- 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 queue
|
||||
|
||||
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 LibLogout(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:
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def logout(
|
||||
self,
|
||||
username: str
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
return False
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
By.XPATH, "//a[@href='/logout']"
|
||||
).click()
|
||||
self._showTrace(f"用户 {username} 注销成功 !")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"用户 {username} 注销失败 ! : {e}")
|
||||
return False
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- 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 base.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
|
||||
@@ -0,0 +1,618 @@
|
||||
# -*- 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 re
|
||||
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 LibReserve(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
# library floor and room mapping in website
|
||||
self.__floor_map = {
|
||||
"2": "二层",
|
||||
"3": "三层",
|
||||
"4": "四层",
|
||||
"5": "五层"
|
||||
}
|
||||
self.__room_map = {
|
||||
"1": "二层内环",
|
||||
"2": "二层外环",
|
||||
"3": "三层内环",
|
||||
"4": "三层外环",
|
||||
"5": "四层内环",
|
||||
"6": "四层外环",
|
||||
"7": "四层期刊区",
|
||||
"8": "五层考研"
|
||||
}
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
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, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".layoutSeat dt"))
|
||||
)
|
||||
title_elements = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".layoutSeat dt"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
content_elements = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".layoutSeat dd"
|
||||
)
|
||||
if not content_elements:
|
||||
self._showTrace("未找到预约结果")
|
||||
raise
|
||||
title = title_elements[0].text if title_elements else ""
|
||||
contents = [element.text for element in content_elements if element.text.strip()]
|
||||
for message in contents:
|
||||
if "预约失败" in message or "已有1个有效预约" in message:
|
||||
self._showTrace(f"预约失败 - {"".join(contents)}")
|
||||
raise
|
||||
if "预定好了" in title or "预约成功" in title or "操作成功" in title:
|
||||
if len(contents) >= 6:
|
||||
self._showTrace(f"\n"\
|
||||
f" 预约成功 !\n"\
|
||||
f" {contents[1]}\n"\
|
||||
f" {contents[2]}\n"\
|
||||
f" {contents[3]}\n"\
|
||||
f" 签到时间 :{contents[5]}")
|
||||
else:
|
||||
self._showTrace(f"\n"\
|
||||
f" 预约成功 !\n"\
|
||||
f" 未找获取到详细信息")
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"预约结果加载失败 !")
|
||||
return False
|
||||
|
||||
@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 __containRequiredInfo(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
# must contain the required infomation
|
||||
if reserve_info.get("floor") is None: # if existence ?
|
||||
raise ValueError("未指定楼层")
|
||||
if reserve_info["floor"] not in self.__floor_map: # if in the mao ?
|
||||
raise ValueError(f"该楼层 '{reserve_info['floor']}' 不存在")
|
||||
if reserve_info.get("room") is None:
|
||||
raise ValueError("未指定房间")
|
||||
if reserve_info["room"] not in self.__room_map:
|
||||
raise ValueError(f"该房间 '{reserve_info['room']}' 不存在")
|
||||
if reserve_info.get("seat_id") is None:
|
||||
raise ValueError("未指定座位")
|
||||
if reserve_info["seat_id"] == "":
|
||||
raise ValueError("未指定座位号")
|
||||
return True
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"预约信息错误 ! : {e}, "\
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def __isValidDate(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
cur_date = time.strftime("%Y-%m-%d", time.localtime())
|
||||
if reserve_info.get("date") is None:
|
||||
reserve_info["date"] = cur_date
|
||||
self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date}")
|
||||
else:
|
||||
if reserve_info["date"] < cur_date:
|
||||
self._showTrace(
|
||||
f"预约日期错误 ! :"\
|
||||
f"{reserve_info['date']} 早于当前日期 {cur_date}, 自动设置为当前日期"
|
||||
)
|
||||
reserve_info["date"] = cur_date
|
||||
return True
|
||||
|
||||
|
||||
def __isValidBeginTime(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
cur_time = time.strftime("%H:%M", time.localtime())
|
||||
if reserve_info.get("begin_time") is None:
|
||||
reserve_info["begin_time"] = {}
|
||||
if "time" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["time"] = cur_time
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {cur_time}")
|
||||
if "max_diff" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["max_diff"] = 30
|
||||
self._showTrace(f"开始时间最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["prefer_early"] = True
|
||||
self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True")
|
||||
return True
|
||||
|
||||
|
||||
def __isValidExpectDuration(
|
||||
self,
|
||||
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("预约满足时长要求未指定, 默认满足")
|
||||
return True
|
||||
|
||||
|
||||
def __isValidEndTime(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
if reserve_info.get("end_time") is None:
|
||||
reserve_info["end_time"] = {}
|
||||
if "time" not in reserve_info["end_time"]:
|
||||
end_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = end_mins + int(reserve_info["expect_duration"]*60)
|
||||
reserve_info["end_time"] = {
|
||||
"time": self.__minsToTime(end_mins),
|
||||
"max_diff": 30,
|
||||
"prefer_early": False
|
||||
}
|
||||
self._showTrace(
|
||||
f"结束时间未指定, 自动设置为开始时间加上期望时长: {reserve_info['end_time']['time']}"
|
||||
)
|
||||
if "max_diff" not in reserve_info["end_time"]:
|
||||
reserve_info["end_time"]["max_diff"] = 30
|
||||
self._showTrace(f"结束时间最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in reserve_info["end_time"]:
|
||||
reserve_info["end_time"]["prefer_early"] = False
|
||||
self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True")
|
||||
return True
|
||||
|
||||
|
||||
def __finalCheck(
|
||||
self,
|
||||
reserve_info: dict
|
||||
):
|
||||
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self.__timeToMins(begin_time["time"])
|
||||
end_mins = self.__timeToMins(end_time["time"])
|
||||
# if end time is earlier than begin_time, exchange them
|
||||
if end_mins < begin_mins:
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 自动交换"
|
||||
)
|
||||
reserve_info["end_time"] = begin_time
|
||||
reserve_info["begin_time"] = end_time
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self.__timeToMins(begin_time["time"])
|
||||
end_mins = self.__timeToMins(end_time["time"])
|
||||
# ensure the end time is not later than 23:30
|
||||
if end_mins > self.__timeToMins("23:30"):
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30"
|
||||
)
|
||||
reserve_info["end_time"]["time"] = "23:30"
|
||||
end_mins = self.__timeToMins("23:30")
|
||||
# ensure the duration is not longer than 8 hours
|
||||
if reserve_info["satisfy_duration"]:
|
||||
if reserve_info["expect_duration"] > 8:
|
||||
self._showTrace(
|
||||
f"该用户设置了优先满足时长要求, 但是预约期望持续时间 "
|
||||
f"{reserve_info['expect_duration']} 小时 "
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时"
|
||||
)
|
||||
reserve_info["expect_duration"] = 8
|
||||
else:
|
||||
if end_mins - begin_mins > 8*60:
|
||||
self._showTrace(
|
||||
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
|
||||
f"{int((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
|
||||
|
||||
|
||||
def __checkReserveInfo(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
if not self.__containRequiredInfo(reserve_info):
|
||||
return False
|
||||
if not self.__isValidDate(reserve_info):
|
||||
return False
|
||||
if not self.__isValidBeginTime(reserve_info):
|
||||
return False
|
||||
if not self.__isValidExpectDuration(reserve_info):
|
||||
return False
|
||||
if not self.__isValidEndTime(reserve_info):
|
||||
return False
|
||||
if not self.__finalCheck(reserve_info):
|
||||
return False
|
||||
self._showTrace(
|
||||
f"预约信息检查完成, 准备预约 "
|
||||
f"{reserve_info['date']} "
|
||||
f"{reserve_info['begin_time']["time"]} - "
|
||||
f"{reserve_info['end_time']["time"]} "
|
||||
f"图书馆 "
|
||||
f"{self.__floor_map[reserve_info['floor']]} "
|
||||
f"{self.__room_map[reserve_info['room']]} "
|
||||
f"的座位 {reserve_info['seat_id']}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def __clickElement(
|
||||
self,
|
||||
trigger_locator: tuple,
|
||||
fail_msg: str,
|
||||
success_msg: str,
|
||||
option_locator: tuple = None
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
# click the trigger element
|
||||
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, 2).until(
|
||||
EC.element_to_be_clickable(option_locator)
|
||||
).click()
|
||||
self._showTrace(success_msg)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(fail_msg)
|
||||
return False
|
||||
|
||||
|
||||
def __selectDate(
|
||||
self,
|
||||
date_str: str
|
||||
) -> bool:
|
||||
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "onDate_select"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"),
|
||||
success_msg=f"日期 {date_str} 选择成功 !",
|
||||
fail_msg=f"选择日期失败 ! : {date_str} 不可用"
|
||||
)
|
||||
|
||||
|
||||
def __selectPlace(
|
||||
self,
|
||||
place: str
|
||||
) -> bool:
|
||||
|
||||
actual_place = "1" if place == "图书馆" else "1"
|
||||
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} 不可用"
|
||||
)
|
||||
|
||||
|
||||
def __selectFloor(
|
||||
self,
|
||||
floor: str
|
||||
) -> bool:
|
||||
|
||||
display_floor = self.__floor_map.get(floor)
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "floor_select"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"),
|
||||
success_msg=f"楼层 {display_floor} 选择成功 !",
|
||||
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
|
||||
)
|
||||
|
||||
|
||||
def __selectRoom(
|
||||
self,
|
||||
room: str
|
||||
) -> 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} 不可用"
|
||||
)
|
||||
|
||||
|
||||
def __selectSeat(
|
||||
self,
|
||||
seat_id: str
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
# wait fot seat layout element to load
|
||||
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_']"
|
||||
)
|
||||
seat_id_upper = seat_id.lstrip('0').upper()
|
||||
for seat in all_seats:
|
||||
if not seat_id_upper == seat.text.lstrip('0'):
|
||||
continue
|
||||
seat_link = seat.find_element(By.TAG_NAME, "a")
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(seat_link)
|
||||
)
|
||||
seat_link.click()
|
||||
seat_status = seat_link.get_attribute("title")
|
||||
self._showTrace(f"座位 {seat_id} 选择成功 ! : 当前状态 - '{seat_status}'")
|
||||
return True
|
||||
self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确")
|
||||
except:
|
||||
self._showTrace(f"座位选择失败 !")
|
||||
return False
|
||||
|
||||
|
||||
def __selectNearestTime(
|
||||
self,
|
||||
time_id: str,
|
||||
time_type: str,
|
||||
target_time: int,
|
||||
max_time_diff: int = 30,
|
||||
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,
|
||||
f"#{time_id} ul li a"
|
||||
)
|
||||
free_times = []
|
||||
best_time_diff = max_time_diff
|
||||
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":
|
||||
now = datetime.now()
|
||||
time_val = int(now.hour*60 + now.minute)
|
||||
elif time_attr and time_attr.isdigit():
|
||||
time_val = int(time_attr)
|
||||
else:
|
||||
continue
|
||||
free_times.append(self.__minsToTime(time_val))
|
||||
actual_diff = time_val - target_time
|
||||
abs_diff = abs(actual_diff)
|
||||
if abs_diff < best_time_diff or (
|
||||
abs_diff == best_time_diff and (
|
||||
# prefer earlier time
|
||||
(prefer_earlier and actual_diff <= 0) or
|
||||
# prefer later time
|
||||
(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"正好等于 {time_type}"
|
||||
target_time += best_actual_diff
|
||||
self._showTrace(
|
||||
f"选择距离期望 {time_type} 最近的 {best_time_opt.text}, "\
|
||||
f"与期望 {time_type} 相比 {time_relation}"
|
||||
)
|
||||
return target_time
|
||||
self._showTrace(
|
||||
f"无法选择最近的 {time_type} {self.__minsToTime(target_time)}, "\
|
||||
f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟"
|
||||
)
|
||||
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
|
||||
return -1
|
||||
except:
|
||||
self._showTrace(f"{time_type} {self.__minsToTime(target_time)} 选择失败 !")
|
||||
return -1
|
||||
|
||||
|
||||
def __selectSeatTime(
|
||||
self,
|
||||
begin_time: dict,
|
||||
end_time: dict,
|
||||
expct_duration: int = 4,
|
||||
satisfy_duration: bool = True
|
||||
) -> bool:
|
||||
|
||||
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)
|
||||
expect_end_mins = self.__timeToMins(expect_end_time)
|
||||
|
||||
# select the begin time
|
||||
if self.__selectNearestTime(
|
||||
time_id="startTime", # dont change into begin, this is the element in the page
|
||||
time_type="开始时间",
|
||||
target_time=expect_begin_mins,
|
||||
max_time_diff=begin_time["max_diff"],
|
||||
prefer_earlier=begin_time["prefer_early"]
|
||||
) == -1:
|
||||
return False
|
||||
else:
|
||||
actual_begin_time = self.__minsToTime(expect_begin_mins)
|
||||
# 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)
|
||||
self._showTrace(
|
||||
f"需要满足期望预约持续时间: {expct_duration} 小时, "\
|
||||
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
|
||||
)
|
||||
# select the end time
|
||||
if self.__selectNearestTime(
|
||||
time_id="endTime",
|
||||
time_type="结束时间",
|
||||
target_time=expect_end_mins,
|
||||
max_time_diff=end_time["max_diff"],
|
||||
prefer_earlier=end_time["prefer_early"]
|
||||
) == -1:
|
||||
return False
|
||||
else:
|
||||
actual_end_time = self.__minsToTime(expect_end_mins)
|
||||
self._showTrace(
|
||||
f"期望预约时间段: {expect_begin_time} - {expect_end_time}, "
|
||||
f"实际预约时间段: {actual_begin_time} - {actual_end_time}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def reserve(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
submit_reserve = False
|
||||
reserve_success = False
|
||||
have_hover_on_page = False
|
||||
|
||||
# reserve info
|
||||
if not self.__checkReserveInfo(reserve_info):
|
||||
return False
|
||||
# map page
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"加载预约选座页面失败 !")
|
||||
return False
|
||||
# date, place, floor
|
||||
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:
|
||||
have_hover_on_page = True
|
||||
# seat selections
|
||||
if not self.__selectSeat(reserve_info["seat_id"]):
|
||||
pass
|
||||
elif not self.__selectSeatTime(
|
||||
begin_time=reserve_info["begin_time"],
|
||||
end_time=reserve_info["end_time"],
|
||||
expct_duration=reserve_info["expect_duration"],
|
||||
satisfy_duration=reserve_info["satisfy_duration"]
|
||||
):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "reserveBtn"))
|
||||
).click()
|
||||
submit_reserve = True
|
||||
if not self._waitResponseLoad():
|
||||
raise
|
||||
reserve_success = True
|
||||
except:
|
||||
self._showTrace(f"预约提交失败 !")
|
||||
if not submit_reserve and have_hover_on_page:
|
||||
self.__driver.refresh()
|
||||
return reserve_success
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- 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 json
|
||||
|
||||
|
||||
class ConfigReader:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_path: str
|
||||
):
|
||||
|
||||
self._config_path = config_path
|
||||
self._config_data = {}
|
||||
if not self.__readConfig():
|
||||
return None
|
||||
|
||||
|
||||
def __readConfig(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
with open(self._config_path, 'r', encoding='utf-8') as file:
|
||||
self._config_data = json.load(file)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error reading config file: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def getConfigs(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return self._config_data.copy()
|
||||
|
||||
|
||||
def getConfig(
|
||||
self,
|
||||
key: str
|
||||
) -> dict:
|
||||
|
||||
return self._config_data.get(key, {})
|
||||
|
||||
|
||||
def get(
|
||||
self,
|
||||
key: str,
|
||||
default: any = None
|
||||
) -> any:
|
||||
|
||||
keys = key.split('/')
|
||||
current = self._config_data
|
||||
for k in keys:
|
||||
if isinstance(current, dict) and k in current:
|
||||
current = current[k]
|
||||
else:
|
||||
return default
|
||||
return current
|
||||
|
||||
|
||||
def hasConfig(
|
||||
self,
|
||||
key: str
|
||||
) -> bool:
|
||||
|
||||
return self.getConfig(key) != {}
|
||||
|
||||
|
||||
def reReadConfig(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return self.__readConfig()
|
||||
|
||||
|
||||
def configPath(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self._config_path
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- 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 json
|
||||
|
||||
|
||||
class ConfigWriter:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_path: str,
|
||||
config_data: dict
|
||||
):
|
||||
|
||||
self.__config_path = config_path
|
||||
self.__config_data = config_data if config_data is not None else {}
|
||||
if config_data is None:
|
||||
return None
|
||||
if not self.__writeConfig():
|
||||
return None
|
||||
|
||||
|
||||
def __writeConfig(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
with open(self.__config_path, "w") as f:
|
||||
json.dump(self.__config_data, f, indent=4, sort_keys=False)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def setConfigs(
|
||||
self,
|
||||
configs: dict
|
||||
) -> bool:
|
||||
|
||||
self.__config_data = configs
|
||||
return self.__writeConfig()
|
||||
|
||||
|
||||
def setConfig(
|
||||
self,
|
||||
key: str,
|
||||
value: dict
|
||||
) -> bool:
|
||||
|
||||
self.__config_data[key] = value
|
||||
return self.__writeConfig()
|
||||
|
||||
|
||||
def set(
|
||||
self,
|
||||
key: str,
|
||||
value: dict
|
||||
) -> bool:
|
||||
|
||||
keys = key.replace("\\", "/").split("/")
|
||||
current = self.__config_data
|
||||
for k in keys[:-1]:
|
||||
if k not in current or not isinstance(current[k], dict):
|
||||
current[k] = {}
|
||||
current = current[k]
|
||||
current[keys[-1]] = value
|
||||
return self.__writeConfig()
|
||||
|
||||
|
||||
def reWriteConfig(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return self.__writeConfig()
|
||||
|
||||
|
||||
def configPath(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.__config_path
|
||||
@@ -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.
|
||||
"""
|
||||
Reference in New Issue
Block a user