mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 07:23:03 +08:00
chore(*): first commit
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
**/build
|
||||
**/dist
|
||||
**/models/*.onnx
|
||||
**/driver/*.exe
|
||||
**/.git
|
||||
**/.vscode
|
||||
**/.stfolder
|
||||
**/.stignore
|
||||
**/gui/AutoLibraryResources.py
|
||||
**/__pycache__
|
||||
**/gui/AutoLibraryResource.py
|
||||
**/gui/Ui_ALMainWindow.py
|
||||
**/gui/Ui_ALConfigWidget.py
|
||||
**/gui/translators/qtbase_zh_CN.qm
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
# -*- 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 MsgBase import MsgBase
|
||||
from LibLogin import LibLogin
|
||||
from LibLogout import LibLogout
|
||||
from LibReserve import LibReserve
|
||||
|
||||
from 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")
|
||||
|
||||
edge_options.add_argument("--window-size=1280,720")
|
||||
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(10)
|
||||
self.__driver.execute_script(
|
||||
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
)
|
||||
except Exception as e:
|
||||
self._showTrace(f"浏览器驱动初始化失败: {e}")
|
||||
return False
|
||||
# init library operators
|
||||
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._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
|
||||
return True
|
||||
|
||||
|
||||
def __waitResponseLoad(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
# wait for page load
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).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:
|
||||
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
if not self.__waitResponseLoad():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __run(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
reserve_info: dict,
|
||||
) -> bool:
|
||||
|
||||
success = False
|
||||
|
||||
# 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 False
|
||||
run_mode = self.__system_config_reader.get("run/mode", 1)
|
||||
run_mode = {
|
||||
"auto_reserve": run_mode&0x1,
|
||||
"auto_checkin": run_mode&0x2,
|
||||
"auto_renewal": run_mode&0x4,
|
||||
}
|
||||
# reserve or checkin or renewal
|
||||
"""
|
||||
Here, we collect the run mode from the config file.
|
||||
"""
|
||||
if self.__lib_reserve.canReserve(reserve_info.get("date")) and run_mode["auto_reserve"]:
|
||||
if self.__lib_reserve.reserve(reserve_info):
|
||||
self._showTrace(f"用户 {username} 预约成功 !")
|
||||
success = True
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
success = False
|
||||
# logout
|
||||
if not self.__lib_logout.logout(
|
||||
username,
|
||||
):
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
return False
|
||||
return success
|
||||
|
||||
|
||||
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
|
||||
|
||||
user_counter = {"current": 0, "success": 0, "failed": 0}
|
||||
users = self.__users_config_reader.get("users")
|
||||
self._showTrace(f"共发现 {len(users)} 个用户, "\
|
||||
f"用户配置文件路径: {self.__users_config_reader.configPath()}")
|
||||
|
||||
for user in users:
|
||||
self._showTrace(f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......")
|
||||
if self.__run(
|
||||
username=user["username"],
|
||||
password=user["password"],
|
||||
reserve_info=user["reserve_info"],
|
||||
):
|
||||
user_counter["success"] += 1
|
||||
else:
|
||||
user_counter["failed"] += 1
|
||||
self._showTrace(f"处理完成, 共计 {user_counter["current"]} 个用户, "\
|
||||
f"成功 {user_counter["success"]} 个用户, "\
|
||||
f"失败 {user_counter["failed"]} 个用户")
|
||||
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,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,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 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:
|
||||
|
||||
pass
|
||||
@@ -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 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
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
# -*- 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 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, 5).until( # title contains "自选座位 :: 座位预约系统"
|
||||
EC.title_contains("自选座位 :: 座位预约系统")
|
||||
)
|
||||
WebDriverWait(self.__driver, 3).until( # search button presence
|
||||
EC.presence_of_element_located((By.ID, "search"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 3).until( # select content presence
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "selectContent"))
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"登录页面加载失败 ! : {e}")
|
||||
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 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
|
||||
while True:
|
||||
if auto_captcha:
|
||||
captcha_text = self.__autoRecognizeCaptcha()
|
||||
else:
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
|
||||
captcha_text = self.__manualRecognizeCaptcha()
|
||||
if captcha_text:
|
||||
break
|
||||
captcha_element = self.__driver.find_element(By.NAME, "answer")
|
||||
captcha_element.clear()
|
||||
captcha_element.send_keys(captcha_text)
|
||||
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,62 @@
|
||||
# -*- 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 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,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 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
|
||||
+34
@@ -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 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
|
||||
+652
@@ -0,0 +1,652 @@
|
||||
# -*- 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 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, 5).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "layoutSeat"))
|
||||
)
|
||||
title_elements = []
|
||||
# reserve failed without title elements, so we need to try
|
||||
try:
|
||||
WebDriverWait(self.__driver, 1).until(
|
||||
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:
|
||||
date_val = contents[1].split(" : ")[1].strip() if " : " in contents[1] else contents[1].strip()
|
||||
time_val = contents[2].split(" : ")[1].strip() if " : " in contents[2] else contents[2].strip()
|
||||
seat_val = contents[3].split(" : ")[1].strip() if " : " in contents[3] else contents[3].strip()
|
||||
checkin_val = contents[5].strip()
|
||||
self._showTrace(f"\n"\
|
||||
f" 预约成功 !\n"\
|
||||
f" 预约日期: {date_val}, \n"\
|
||||
f" 预约时间: {time_val}, \n"\
|
||||
f" 预约座位: {seat_val}, \n"\
|
||||
f" 签到时间: {checkin_val}")
|
||||
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 __checkReserveInfo(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
# check the required information
|
||||
# reserve_info["place"]
|
||||
if reserve_info.get("floor") is None:
|
||||
raise ValueError("未指定楼层")
|
||||
if reserve_info["floor"] not in self.__floor_map:
|
||||
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("未指定座位")
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"预约信息错误 ! : {e}, "\
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
|
||||
)
|
||||
return False
|
||||
|
||||
# check and try to fix the time errors
|
||||
cur_time_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
||||
cur_date, curr_time = cur_time_str.split()
|
||||
if not reserve_info.get("date"):
|
||||
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
|
||||
# check the begin time
|
||||
begin_time = reserve_info.get("begin_time")
|
||||
if not begin_time:
|
||||
reserve_info["begin_time"] = {
|
||||
"time": curr_time,
|
||||
"max_diff": 30,
|
||||
"prefer_early": True
|
||||
}
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}, 最大时间差为 30 分钟, 优先选择更早预约时间")
|
||||
else:
|
||||
begin_time = reserve_info["begin_time"]
|
||||
if "time" not in begin_time:
|
||||
begin_time["time"] = curr_time
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}")
|
||||
if "max_diff" not in begin_time:
|
||||
begin_time["max_diff"] = 30
|
||||
self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in begin_time:
|
||||
begin_time["prefer_early"] = True
|
||||
self._showTrace(f"是否优先选择更早预约时间未指定, 自动设置为 True")
|
||||
expect_duration = reserve_info.get("expect_duration")
|
||||
if not expect_duration:
|
||||
reserve_info["expect_duration"] = 4
|
||||
expect_duration = 4
|
||||
self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
|
||||
if not reserve_info.get("satisfy_duration"):
|
||||
reserve_info["satisfy_duration"] = True
|
||||
self._showTrace("预约满足时长要求未指定, 默认满足")
|
||||
# check the end time
|
||||
if not reserve_info.get("end_time"):
|
||||
begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = begin_mins + reserve_info["expect_duration"] * 60
|
||||
end_time_str = self.__minsToTime(end_mins)
|
||||
reserve_info["end_time"] = {
|
||||
"time": end_time_str,
|
||||
"max_diff": 30,
|
||||
"prefer_early": False
|
||||
}
|
||||
self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time_str}, 最大时间差为 30 分钟, 优先选择较晚预约时间")
|
||||
else:
|
||||
end_time = reserve_info["end_time"]
|
||||
if "time" not in end_time:
|
||||
begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = begin_mins + reserve_info["expect_duration"] * 60
|
||||
end_time["time"] = self.__minsToTime(end_mins)
|
||||
self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time['time']}")
|
||||
if "max_diff" not in end_time:
|
||||
end_time["max_diff"] = 30
|
||||
self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in end_time:
|
||||
end_time["prefer_early"] = False
|
||||
self._showTrace(f"是否优先选择较早预约时间未指定, 自动设置为 False")
|
||||
# check the reserve time boundary and fix the errors
|
||||
#
|
||||
# get time string for message show
|
||||
begin_time_str = reserve_info["begin_time"]["time"]
|
||||
end_time_str = reserve_info["end_time"]["time"]
|
||||
|
||||
# minute time for check and fix them
|
||||
begin_mins = self.__timeToMins(begin_time_str)
|
||||
end_mins = self.__timeToMins(end_time_str)
|
||||
|
||||
# ensure begin time is not later than end time
|
||||
if begin_mins > end_mins:
|
||||
reserve_info["begin_time"]["time"], reserve_info["end_time"]["time"] = end_time_str, begin_time_str
|
||||
reserve_info["begin_time"]["prefer_early"], reserve_info["end_time"]["prefer_early"] = \
|
||||
reserve_info["end_time"]["prefer_early"], reserve_info["begin_time"]["prefer_early"]
|
||||
self._showTrace("预约开始时间晚于预约结束时间,自动调换开始时间和结束时间")
|
||||
|
||||
# update the begin_mins and end_mins after swap
|
||||
begin_time_str, end_time_str = end_time_str, begin_time_str
|
||||
begin_mins, end_mins = end_mins, begin_mins
|
||||
|
||||
# ensure end time is not later than 22:30
|
||||
max_end_mins = self.__timeToMins("22:30")
|
||||
if end_mins > max_end_mins:
|
||||
reserve_info["end_time"]["time"] = "22:30"
|
||||
end_time_str = "22:30"
|
||||
end_mins = max_end_mins
|
||||
self._showTrace("预约结束时间超过 22:30, 自动设置为 22:30")
|
||||
|
||||
# ensure expect duration is shorter than 8 hours
|
||||
max_duration_mins = 8 * 60
|
||||
duration_mins = end_mins - begin_mins
|
||||
if duration_mins > max_duration_mins:
|
||||
new_end_mins = begin_mins + max_duration_mins
|
||||
reserve_info["end_time"]["time"] = self.__minsToTime(new_end_mins)
|
||||
self._showTrace("预约持续时间超过8小时, 自动设置为 8 小时")
|
||||
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, 5).until(
|
||||
EC.element_to_be_clickable(trigger_locator)
|
||||
).click()
|
||||
if option_locator:
|
||||
# select the option element if specified
|
||||
WebDriverWait(self.__driver, 5).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, 5).until(
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
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, 5).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:
|
||||
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
|
||||
|
||||
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 canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
if date is None:
|
||||
self._showTrace("日期未指定, 无法检查预约状态")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"正在检查用户在日期 {date} 是否可预约......")
|
||||
date_obj = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
try:
|
||||
# we need to navigate to the history page to check if we can reserve
|
||||
WebDriverWait(self.__driver, 5).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"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList dl"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
checked_count = 0
|
||||
max_attemots = 3 # we only check (3*4=)12 reservations
|
||||
|
||||
for _ in range(max_attemots):
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList dl"
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return False
|
||||
for i in range(checked_count, len(reservations) - 1): # the last one is load button
|
||||
reservation = reservations[i]
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
status_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
is_reserved = any("已预约" in status.text for status in status_elements)
|
||||
# process time element to get the date string
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r'(\d{4}-\d{1,2}-\d{1,2})', time_str)
|
||||
if date_match:
|
||||
date_str = date_match.group(1)
|
||||
else:
|
||||
continue
|
||||
# reservation is earlier than the given date, can reserve
|
||||
if datetime.strptime(date_str, "%Y-%m-%d").date() < date_obj:
|
||||
self._showTrace(f"用户在 {date} 可预约")
|
||||
return True
|
||||
# reservation is later than the given date, check the next one
|
||||
elif datetime.strptime(date_str, "%Y-%m-%d").date() > date_obj:
|
||||
continue
|
||||
# compare with the given date
|
||||
if date_str == date and is_reserved:
|
||||
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
except:
|
||||
self._showTrace(f"解析第 {i + 1} 条预约记录时发生未知错误 !")
|
||||
continue
|
||||
checked_count = len(reservations) - 1
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
else:
|
||||
break
|
||||
except:
|
||||
break
|
||||
self._showTrace(f"用户在 {date} 可预约")
|
||||
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, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 5).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, 5).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, 5).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,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()
|
||||
+65
@@ -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,15 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
ddddocr = "*"
|
||||
selenium = "*"
|
||||
pyinstaller = "*"
|
||||
pyside6 = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.13"
|
||||
Generated
+630
@@ -0,0 +1,630 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "26dffc26812d5328611959b95713a7ed65e20c08c60089b54283b0f406dd08e4"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.13"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"altgraph": {
|
||||
"hashes": [
|
||||
"sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406",
|
||||
"sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"
|
||||
],
|
||||
"version": "==0.17.4"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
|
||||
"sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==25.4.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de",
|
||||
"sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2025.10.5"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
|
||||
"sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
|
||||
"sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
|
||||
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
|
||||
"sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
|
||||
"sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
|
||||
"sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
|
||||
"sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
|
||||
"sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
|
||||
"sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
|
||||
"sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
|
||||
"sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
|
||||
"sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
|
||||
"sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
|
||||
"sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
|
||||
"sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
|
||||
"sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
|
||||
"sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
|
||||
"sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
|
||||
"sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
|
||||
"sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
|
||||
"sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
|
||||
"sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
|
||||
"sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
|
||||
"sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
|
||||
"sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
|
||||
"sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
|
||||
"sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
|
||||
"sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
|
||||
"sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
|
||||
"sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
|
||||
"sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
|
||||
"sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
|
||||
"sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
|
||||
"sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
|
||||
"sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
|
||||
"sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
|
||||
"sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
|
||||
"sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
|
||||
"sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
|
||||
"sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
|
||||
"sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
|
||||
"sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
|
||||
"sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
|
||||
"sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
|
||||
"sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
|
||||
"sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
|
||||
"sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
|
||||
"sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
|
||||
"sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
|
||||
"sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
|
||||
"sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
|
||||
"sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
|
||||
"sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
|
||||
"sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
|
||||
"sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
|
||||
"sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
|
||||
"sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
|
||||
"sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
|
||||
"sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
|
||||
"sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
|
||||
"sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
|
||||
"sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
|
||||
"sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
|
||||
"sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
|
||||
"sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
|
||||
"sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
|
||||
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
|
||||
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
|
||||
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
|
||||
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
|
||||
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
|
||||
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
|
||||
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
|
||||
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
|
||||
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
|
||||
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
|
||||
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
|
||||
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
|
||||
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
|
||||
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
|
||||
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
|
||||
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
|
||||
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"coloredlogs": {
|
||||
"hashes": [
|
||||
"sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934",
|
||||
"sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==15.0.1"
|
||||
},
|
||||
"ddddocr": {
|
||||
"hashes": [
|
||||
"sha256:5991594d481d33ba0b136022e910f578d6d5b0ca536b44886591359622ab0c70",
|
||||
"sha256:7c44b58ba7d7566d785c65b8526ec5b78efacd121e993dea4fda5f7966897428"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.0.6"
|
||||
},
|
||||
"flatbuffers": {
|
||||
"hashes": [
|
||||
"sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2",
|
||||
"sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12"
|
||||
],
|
||||
"version": "==25.9.23"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
|
||||
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"humanfriendly": {
|
||||
"hashes": [
|
||||
"sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
|
||||
"sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==10.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
|
||||
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.11"
|
||||
},
|
||||
"mpmath": {
|
||||
"hashes": [
|
||||
"sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f",
|
||||
"sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"numpy": {
|
||||
"hashes": [
|
||||
"sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64",
|
||||
"sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e",
|
||||
"sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0",
|
||||
"sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365",
|
||||
"sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d",
|
||||
"sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c",
|
||||
"sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52",
|
||||
"sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36",
|
||||
"sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec",
|
||||
"sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f",
|
||||
"sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197",
|
||||
"sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7",
|
||||
"sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9",
|
||||
"sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37",
|
||||
"sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a",
|
||||
"sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db",
|
||||
"sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c",
|
||||
"sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7",
|
||||
"sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d",
|
||||
"sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e",
|
||||
"sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f",
|
||||
"sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a",
|
||||
"sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16",
|
||||
"sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e",
|
||||
"sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868",
|
||||
"sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05",
|
||||
"sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e",
|
||||
"sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff",
|
||||
"sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f",
|
||||
"sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7",
|
||||
"sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f",
|
||||
"sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e",
|
||||
"sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562",
|
||||
"sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6",
|
||||
"sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0",
|
||||
"sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26",
|
||||
"sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0",
|
||||
"sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d",
|
||||
"sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879",
|
||||
"sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef",
|
||||
"sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29",
|
||||
"sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252",
|
||||
"sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847",
|
||||
"sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6",
|
||||
"sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32",
|
||||
"sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0",
|
||||
"sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3",
|
||||
"sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b",
|
||||
"sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3",
|
||||
"sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc",
|
||||
"sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc",
|
||||
"sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda",
|
||||
"sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a",
|
||||
"sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40",
|
||||
"sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032",
|
||||
"sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7",
|
||||
"sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966",
|
||||
"sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9",
|
||||
"sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346",
|
||||
"sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2",
|
||||
"sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a",
|
||||
"sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786",
|
||||
"sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f",
|
||||
"sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc",
|
||||
"sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb",
|
||||
"sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646",
|
||||
"sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd",
|
||||
"sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1",
|
||||
"sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11",
|
||||
"sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667",
|
||||
"sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996",
|
||||
"sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953",
|
||||
"sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b",
|
||||
"sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"
|
||||
],
|
||||
"markers": "python_version >= '3.11'",
|
||||
"version": "==2.3.4"
|
||||
},
|
||||
"onnxruntime": {
|
||||
"hashes": [
|
||||
"sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc",
|
||||
"sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77",
|
||||
"sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95",
|
||||
"sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088",
|
||||
"sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b",
|
||||
"sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435",
|
||||
"sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f",
|
||||
"sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b",
|
||||
"sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7",
|
||||
"sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c",
|
||||
"sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c",
|
||||
"sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612",
|
||||
"sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872",
|
||||
"sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2",
|
||||
"sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e",
|
||||
"sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466",
|
||||
"sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3",
|
||||
"sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36",
|
||||
"sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321",
|
||||
"sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6",
|
||||
"sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e",
|
||||
"sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==1.23.2"
|
||||
},
|
||||
"outcome": {
|
||||
"hashes": [
|
||||
"sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8",
|
||||
"sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.0.post0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
|
||||
"sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==25.0"
|
||||
},
|
||||
"pefile": {
|
||||
"hashes": [
|
||||
"sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc",
|
||||
"sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==2023.2.7"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643",
|
||||
"sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e",
|
||||
"sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e",
|
||||
"sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc",
|
||||
"sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642",
|
||||
"sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6",
|
||||
"sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1",
|
||||
"sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b",
|
||||
"sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399",
|
||||
"sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba",
|
||||
"sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad",
|
||||
"sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47",
|
||||
"sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739",
|
||||
"sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b",
|
||||
"sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f",
|
||||
"sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10",
|
||||
"sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52",
|
||||
"sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d",
|
||||
"sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b",
|
||||
"sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a",
|
||||
"sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9",
|
||||
"sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d",
|
||||
"sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098",
|
||||
"sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905",
|
||||
"sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b",
|
||||
"sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3",
|
||||
"sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371",
|
||||
"sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953",
|
||||
"sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01",
|
||||
"sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca",
|
||||
"sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e",
|
||||
"sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7",
|
||||
"sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27",
|
||||
"sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082",
|
||||
"sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e",
|
||||
"sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d",
|
||||
"sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8",
|
||||
"sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a",
|
||||
"sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad",
|
||||
"sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3",
|
||||
"sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a",
|
||||
"sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d",
|
||||
"sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353",
|
||||
"sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee",
|
||||
"sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b",
|
||||
"sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b",
|
||||
"sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a",
|
||||
"sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7",
|
||||
"sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef",
|
||||
"sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a",
|
||||
"sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a",
|
||||
"sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257",
|
||||
"sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07",
|
||||
"sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4",
|
||||
"sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c",
|
||||
"sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c",
|
||||
"sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4",
|
||||
"sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe",
|
||||
"sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8",
|
||||
"sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5",
|
||||
"sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6",
|
||||
"sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e",
|
||||
"sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8",
|
||||
"sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e",
|
||||
"sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275",
|
||||
"sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3",
|
||||
"sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76",
|
||||
"sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227",
|
||||
"sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9",
|
||||
"sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5",
|
||||
"sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79",
|
||||
"sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca",
|
||||
"sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa",
|
||||
"sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b",
|
||||
"sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e",
|
||||
"sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197",
|
||||
"sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab",
|
||||
"sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79",
|
||||
"sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2",
|
||||
"sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363",
|
||||
"sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0",
|
||||
"sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e",
|
||||
"sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782",
|
||||
"sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925",
|
||||
"sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0",
|
||||
"sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b",
|
||||
"sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced",
|
||||
"sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c",
|
||||
"sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344",
|
||||
"sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9",
|
||||
"sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==12.0.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954",
|
||||
"sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995",
|
||||
"sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef",
|
||||
"sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455",
|
||||
"sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee",
|
||||
"sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9",
|
||||
"sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3",
|
||||
"sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035",
|
||||
"sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90",
|
||||
"sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==6.33.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
|
||||
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.23"
|
||||
},
|
||||
"pyinstaller": {
|
||||
"hashes": [
|
||||
"sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058",
|
||||
"sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef",
|
||||
"sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8",
|
||||
"sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851",
|
||||
"sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a",
|
||||
"sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3",
|
||||
"sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0",
|
||||
"sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0",
|
||||
"sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41",
|
||||
"sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454",
|
||||
"sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64",
|
||||
"sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version < '3.15' and python_version >= '3.8'",
|
||||
"version": "==6.16.0"
|
||||
},
|
||||
"pyinstaller-hooks-contrib": {
|
||||
"hashes": [
|
||||
"sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6",
|
||||
"sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2025.9"
|
||||
},
|
||||
"pyreadline3": {
|
||||
"hashes": [
|
||||
"sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7",
|
||||
"sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.5.4"
|
||||
},
|
||||
"pyside6": {
|
||||
"hashes": [
|
||||
"sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339",
|
||||
"sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f",
|
||||
"sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820",
|
||||
"sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01",
|
||||
"sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"pyside6-addons": {
|
||||
"hashes": [
|
||||
"sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464",
|
||||
"sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0",
|
||||
"sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c",
|
||||
"sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1",
|
||||
"sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"
|
||||
],
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"pyside6-essentials": {
|
||||
"hashes": [
|
||||
"sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a",
|
||||
"sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8",
|
||||
"sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1",
|
||||
"sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998",
|
||||
"sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"
|
||||
],
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"pysocks": {
|
||||
"hashes": [
|
||||
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
|
||||
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
|
||||
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
"sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8",
|
||||
"sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.2.3"
|
||||
},
|
||||
"selenium": {
|
||||
"hashes": [
|
||||
"sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c",
|
||||
"sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==4.38.0"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922",
|
||||
"sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==80.9.0"
|
||||
},
|
||||
"shiboken6": {
|
||||
"hashes": [
|
||||
"sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717",
|
||||
"sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543",
|
||||
"sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148",
|
||||
"sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61",
|
||||
"sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"
|
||||
],
|
||||
"markers": "python_version < '3.14' and python_version >= '3.9'",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
|
||||
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
"hashes": [
|
||||
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
|
||||
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
|
||||
],
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"sympy": {
|
||||
"hashes": [
|
||||
"sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517",
|
||||
"sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"trio": {
|
||||
"hashes": [
|
||||
"sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b",
|
||||
"sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==0.32.0"
|
||||
},
|
||||
"trio-websocket": {
|
||||
"hashes": [
|
||||
"sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae",
|
||||
"sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.12.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
|
||||
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==4.15.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"extras": [
|
||||
"socks"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760",
|
||||
"sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98",
|
||||
"sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"wsproto": {
|
||||
"hashes": [
|
||||
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
|
||||
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==1.2.0"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
@@ -0,0 +1,847 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AutoLibrary 操作手册</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #2c3e50;
|
||||
--secondary: #3498db;
|
||||
--accent: #e74c3c;
|
||||
--light: #f8f9fa;
|
||||
--dark: #2c3e50;
|
||||
--gray: #6c757d;
|
||||
--border: #dee2e6;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #c0c0c0a4;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.manual-container {
|
||||
display: flex;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: var(--primary);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 2rem 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-header:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 5px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover,
|
||||
.sidebar-nav a.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
background: rgb(245, 245, 245);
|
||||
padding: 2rem 3rem;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--primary);
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--secondary);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--dark);
|
||||
margin: 1.5rem 0 1rem;
|
||||
}
|
||||
|
||||
.intro-box {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid var(--secondary);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
counter-reset: step-counter;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
margin-bottom: 2rem;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
counter-increment: step-counter;
|
||||
background: var(--secondary);
|
||||
color: white;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
margin-right: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.step-number::before {
|
||||
content: counter(step-counter);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
background: #e9ecef;
|
||||
border-radius: 5px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--gray);
|
||||
margin: 1rem 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.note {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #f8d7da;
|
||||
border-left: 4px solid #dc3545;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
border: 1px solid #444;
|
||||
border-left: 4px solid var(--secondary);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: 'Consolas', monospace;
|
||||
white-space: pre-wrap;
|
||||
border-radius: 0 5px 5px 0;
|
||||
font-size: 0.9rem;
|
||||
overflow-x: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.code-block .key { color: #2b997f; }
|
||||
.code-block .string { color: #9acf65; }
|
||||
.code-block .number { color: #3dd942; }
|
||||
.code-block .boolean { color: #24e9ff; }
|
||||
.code-block .null { color: #ff79c6; }
|
||||
.code-block .property { color: #8be9fd; }
|
||||
.code-block .punctuation { color: #f8f8f2; }
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.config-tab {
|
||||
padding: 0.8rem 1.5rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1rem;
|
||||
color: var(--gray);
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.config-tab.active {
|
||||
color: var(--secondary);
|
||||
border-bottom: 3px solid var(--secondary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: white;
|
||||
border-radius: 0 5px 5px 5px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: var(--secondary);
|
||||
color: white;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 3px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #2980b9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.faq-question {
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--light);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
padding: 1rem 1.5rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.faq-item.active .faq-answer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.browser-drivers {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.browser-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.browser-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.browser-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.browser-logo img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.browser-card h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.browser-card .btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.manual-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="manual-container">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>AutoLibrary</h1>
|
||||
<p>操作手册 v0.01</p>
|
||||
</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="#intro" class="active">工具简介</a></li>
|
||||
<li><a href="#preparation">准备工作</a></li>
|
||||
<li><a href="#usage">使用步骤</a></li>
|
||||
<li><a href="#configuration">配置说明</a></li>
|
||||
<li><a href="#features">功能详解</a></li>
|
||||
<li><a href="#interface">用户界面</a></li>
|
||||
<li><a href="#troubleshooting">故障排除</a></li>
|
||||
<li><a href="#faq">常见问题</a></li>
|
||||
<li><a href="#download">下载安装</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<section id="name" class="section">
|
||||
<h1>AutoLibrary</h1>
|
||||
</section>
|
||||
|
||||
<section id="intro" class="section">
|
||||
<h2>工具简介</h2>
|
||||
<div class="step">
|
||||
<div class="step-content">
|
||||
<div class="intro-box">
|
||||
<p>AutoLibrary 是一款专为北京建筑大学图书馆设计的自动化工具,旨在帮助学生简化图书馆座位操作流程,节省宝贵时间。</p>
|
||||
</div>
|
||||
<p>本工具模拟人工操作,通过简单的界面交互使用。</p>
|
||||
|
||||
<h3>工具特点</h3>
|
||||
<ul>
|
||||
<p>模拟人工操作,不干扰图书馆系统正常运行</p>
|
||||
<p>支持多种预约模式,满足不同使用场景</p>
|
||||
<p>支持多账号批量预约</p>
|
||||
<p>自动处理验证码,减少人工干预</p>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="preparation" class="section">
|
||||
<h2>准备工作</h2>
|
||||
<div class="step-container">
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>下载浏览器驱动</h3>
|
||||
<p>工具需要通过浏览器驱动来控制浏览器,请根据您使用的浏览器下载对应版本的驱动:</p>
|
||||
|
||||
<div class="browser-drivers">
|
||||
<div class="browser-card">
|
||||
<div class="browser-logo">
|
||||
<img src="https://edgestatic.azureedge.net/welcome/static/favicon.png" alt="Microsoft Edge">
|
||||
</div>
|
||||
<h4>Microsoft Edge</h4>
|
||||
<p>适用于Windows 10/11系统</p>
|
||||
<a href="https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/" target="_blank" class="btn">下载驱动</a>
|
||||
</div>
|
||||
|
||||
<div class="browser-card">
|
||||
<div class="browser-logo">
|
||||
<img src="https://www.gstatic.cn/devrel-devsite/prod/v154b6c17f7870ab2939b3d571919274f806798dc59971188e1f4183601ea7775/chrome/images/touchicon-180.png" alt="Google Chrome">
|
||||
</div>
|
||||
<h4>Google Chrome</h4>
|
||||
<p>最常用的浏览器</p>
|
||||
<a href="https://developer.chrome.google.cn/docs/chromedriver/downloads" target="_blank" class="btn">下载驱动</a>
|
||||
</div>
|
||||
|
||||
<div class="browser-card">
|
||||
<div class="browser-logo">
|
||||
<img src="https://www.firefox.com/media/img/favicons/firefox/browser/favicon-196x196.59e3822720be.png" alt="Mozilla Firefox">
|
||||
</div>
|
||||
<h4>Mozilla Firefox</h4>
|
||||
<p>开源浏览器</p>
|
||||
<a href="https://github.com/mozilla/geckodriver/releases" target="_blank" class="btn">下载驱动</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>注意:</strong> 浏览器驱动版本必须与您的浏览器版本兼容,否则工具无法正常工作。
|
||||
</div>
|
||||
<div class="step-image">
|
||||
浏览器驱动下载页面截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>确认驱动路径</h3>
|
||||
<p>下载驱动后,将浏览器驱动程序的路径(如'C:\Users\Administrator\Downloads\msedgedriver.exe')输入到AutoLibrary配置界面中。</p>
|
||||
<div class="step-image">
|
||||
驱动文件放置位置截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="usage" class="section">
|
||||
<h2>使用步骤</h2>
|
||||
<div class="step-container">
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>编辑配置文件</h3>
|
||||
<p>使用文本编辑器(如记事本、Visual Studio Code等)打开config.json和users.json文件,按照您的需求修改参数。</p>
|
||||
<div class="warning">
|
||||
<strong>重要:</strong> 请勿使用Microsoft Word等富文本编辑器,这可能导致文件格式错误。
|
||||
</div>
|
||||
<div class="step-image">
|
||||
配置文件编辑截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>运行工具</h3>
|
||||
<p>双击运行main.exe文件,工具将自动开始预约流程。</p>
|
||||
<div class="step-image">
|
||||
工具运行界面截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>监控运行状态</h3>
|
||||
<p>如果headless模式设置为false,您将看到浏览器窗口自动操作。请勿手动干预浏览器窗口。</p>
|
||||
<div class="step-image">
|
||||
浏览器自动操作截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>查看结果</h3>
|
||||
<p>工具运行完成后,查看生成的日志文件确认预约结果。</p>
|
||||
<div class="step-image">
|
||||
运行日志截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="configuration" class="section">
|
||||
<h2>配置说明</h2>
|
||||
<p>AutoLibrary通过两个配置文件来控制工具行为:config.json(工具设置)和users.json(用户信息)。</p>
|
||||
|
||||
<div class="config-tabs">
|
||||
<button class="config-tab active" data-tab="config-tab">config.json</button>
|
||||
<button class="config-tab" data-tab="users-tab">users.json</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="config-tab" class="tab-pane active">
|
||||
<h3>工具配置文件</h3>
|
||||
<p>config.json文件控制工具的基本运行参数:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"library"</span>: {
|
||||
<span class="property">"lib_host_url"</span>: <span class="string">"http://10.1.20.7"</span>,
|
||||
<span class="property">"lib_login_url"</span>: <span class="string">"/login"</span>
|
||||
},
|
||||
<span class="property">"mode"</span>: {
|
||||
<span class="property">"run_mode"</span>: <span class="number">1</span>
|
||||
},
|
||||
<span class="property">"login"</span>: {
|
||||
<span class="property">"auto_captcha"</span>: <span class="key">true</span>,
|
||||
<span class="property">"login_attempt"</span>: <span class="number">3</span>
|
||||
},
|
||||
<span class="property">"web_driver"</span>: {
|
||||
<span class="property">"driver_type"</span>: <span class="string">"edge"</span>,
|
||||
<span class="property">"driver_path"</span>: <span class="string">"msedgedriver.exe"</span>,
|
||||
<span class="property">"headless"</span>: <span class="key">false</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ul>
|
||||
<li><strong>run_mode</strong>: 运行模式,可组合使用(1+4+8=13)</li>
|
||||
<li><strong>auto_captcha</strong>: 自动验证码识别,建议保持true</li>
|
||||
<li><strong>login_attempt</strong>: 登录尝试次数,默认3次</li>
|
||||
<li><strong>driver_type</strong>: 浏览器类型(edge/chrome/firefox)</li>
|
||||
<li><strong>driver_path</strong>: 驱动文件路径</li>
|
||||
<li><strong>headless</strong>: 无头模式,false会显示浏览器窗口</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="users-tab" class="tab-pane">
|
||||
<h3>用户配置文件</h3>
|
||||
<p>users.json文件包含用户账号和预约信息:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"users"</span>: [
|
||||
{
|
||||
<span class="property">"username"</span>: <span class="string">"您的学号"</span>,
|
||||
<span class="property">"password"</span>: <span class="string">"您的密码"</span>,
|
||||
<span class="property">"reserve_info"</span>: {
|
||||
<span class="property">"date"</span>: <span class="string">"2025-10-30"</span>,
|
||||
<span class="property">"start_time"</span>: <span class="string">"09:30"</span>,
|
||||
<span class="property">"end_time"</span>: <span class="string">"17:00"</span>,
|
||||
<span class="property">"place"</span>: <span class="string">"1"</span>,
|
||||
<span class="property">"floor"</span>: <span class="string">"4"</span>,
|
||||
<span class="property">"room"</span>: <span class="string">"5"</span>,
|
||||
<span class="property">"seat_id"</span>: <span class="string">"31A"</span>,
|
||||
<span class="property">"expect_duration"</span>: <span class="number">6</span>
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ul>
|
||||
<li><strong>username</strong>: 学号</li>
|
||||
<li><strong>password</strong>: 密码</li>
|
||||
<li><strong>date</strong>: 预约日期(格式:YYYY-MM-DD)</li>
|
||||
<li><strong>start_time/end_time</strong>: 预约时间段</li>
|
||||
<li><strong>place/floor/room</strong>: 图书馆位置信息</li>
|
||||
<li><strong>seat_id</strong>: 座位编号(重要)</li>
|
||||
<li><strong>expect_duration</strong>: 期望使用时长(小时)</li>
|
||||
</ul>
|
||||
<div class="note">
|
||||
<strong>提示:</strong> 可以添加多个用户,工具会按顺序处理每个用户的预约请求。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" class="section">
|
||||
<h2>功能详解</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⏰</div>
|
||||
<h3>自动预约(模式 +1)</h3>
|
||||
<p>当您当前没有有效预约时,工具会自动为您预约指定座位。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 提前预约第二天的座位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">✅</div>
|
||||
<h3>自动签到(模式 +4)</h3>
|
||||
<p>如果您已有预约,且在可签到时间范围内,工具会自动完成签到。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 避免因忘记签到而失去座位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h3>自动续约(模式 +8)</h3>
|
||||
<p>当您正在使用座位且到达可续约时间时,工具会自动延长使用时间。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 需要长时间使用座位的情况
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>模式组合使用</h3>
|
||||
<p>运行模式可以组合使用,只需将对应模式的数值相加:</p>
|
||||
<ul>
|
||||
<li>自动预约 + 自动签到 + 自动续约 = 13(推荐)</li>
|
||||
<li>自动预约 = 1</li>
|
||||
<li>自动预约 + 自动签到 = 5</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="troubleshooting" class="section">
|
||||
<h2>故障排除</h2>
|
||||
<h3>常见问题及解决方法</h3>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">工具启动时报错"无法找到驱动"</div>
|
||||
<div class="faq-answer">
|
||||
<p>这是因为浏览器驱动未正确安装或版本不匹配。</p>
|
||||
<ul>
|
||||
<li>检查驱动文件是否放置在正确位置</li>
|
||||
<li>确认驱动版本与浏览器版本完全匹配</li>
|
||||
<li>尝试重新下载并安装驱动</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">登录失败,提示账号密码错误</div>
|
||||
<div class="faq-answer">
|
||||
<p>请检查users.json文件中的账号密码是否正确。</p>
|
||||
<ul>
|
||||
<li>确认学号和密码无误</li>
|
||||
<li>检查是否有特殊字符需要转义</li>
|
||||
<li>尝试手动登录图书馆系统确认账号可用</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">预约失败,提示座位不可用</div>
|
||||
<div class="faq-answer">
|
||||
<p>目标座位可能已被他人预约或不在可预约时间。</p>
|
||||
<ul>
|
||||
<li>确认座位编号是否正确</li>
|
||||
<li>检查预约时间是否符合图书馆规定</li>
|
||||
<li>尝试预约其他座位或调整预约时间</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="faq" class="section">
|
||||
<h2>常见问题</h2>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">使用AutoLibrary是否安全?</div>
|
||||
<div class="faq-answer">
|
||||
<p>AutoLibrary完全模拟人工操作,不干扰图书馆系统正常运行。工具不会收集或上传您的个人信息,所有数据仅保存在本地配置文件中。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">可以同时预约多个座位吗?</div>
|
||||
<div class="faq-answer">
|
||||
<p>根据图书馆规定,每个账号同一时间段只能预约一个座位。但您可以在users.json中添加多个账号,工具会依次处理每个账号的预约请求。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">工具运行期间可以操作电脑吗?</div>
|
||||
<div class="faq-answer">
|
||||
<p>可以正常使用电脑,但请勿操作工具自动打开的浏览器窗口,否则可能会干扰工具的正常运行。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="download" class="section">
|
||||
<h2>下载安装</h2>
|
||||
<div class="download-section">
|
||||
<h3>获取AutoLibrary</h3>
|
||||
<p>点击下方按钮下载最新版本的AutoLibrary工具包:</p>
|
||||
<a href="#" class="btn">下载 AutoLibrary v0.01</a>
|
||||
<div class="note" style="margin-top: 1.5rem;">
|
||||
<p>文件大小:约15MB</p>
|
||||
<p>系统要求:Windows 10/11,支持Edge/Chrome/Firefox浏览器</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>安装步骤</h3>
|
||||
<ol>
|
||||
<li>下载压缩包并解压到任意文件夹</li>
|
||||
<li>根据您使用的浏览器下载对应版本的驱动</li>
|
||||
<li>将驱动文件放置到工具文件夹中</li>
|
||||
<li>按照本手册说明编辑配置文件</li>
|
||||
<li>双击main.exe运行工具</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
||||
a.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetSection = document.querySelector(targetId);
|
||||
|
||||
if (targetSection) {
|
||||
targetSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.config-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
|
||||
document.querySelectorAll('.config-tab').forEach(t => {
|
||||
t.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||||
pane.classList.remove('active');
|
||||
});
|
||||
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.faq-question').forEach(question => {
|
||||
question.addEventListener('click', function() {
|
||||
const faqItem = this.parentElement;
|
||||
faqItem.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
const sections = document.querySelectorAll('.section');
|
||||
const navLinks = document.querySelectorAll('.sidebar-nav a');
|
||||
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '0px 0px -50% 0px',
|
||||
threshold: 0.2
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.getAttribute('id');
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
|
||||
const activeLink = document.querySelector(`.sidebar-nav a[href="#${id}"]`);
|
||||
if (activeLink) {
|
||||
activeLink.classList.add('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
sections.forEach(section => {
|
||||
observer.observe(section);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
This folder is used to store the browser driver by selenium.
|
||||
@@ -0,0 +1,800 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QLineEdit, QMessageBox, QFileDialog, QListWidgetItem
|
||||
)
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
|
||||
from .Ui_ALConfigWidget import Ui_ALConfigWidget
|
||||
|
||||
from ConfigReader import ConfigReader
|
||||
from ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
configWidgetCloseSingal = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
"users":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
|
||||
}
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.modifyUi()
|
||||
self.__config_paths = config_paths
|
||||
self.__system_config_data = self.loadSystemConfig(self.__config_paths["system"])
|
||||
self.__users_config_data = self.loadUsersConfig(self.__config_paths["users"])
|
||||
if not self.__system_config_data:
|
||||
self.initlizeDefaultConfig("system")
|
||||
if not self.__users_config_data:
|
||||
self.initlizeDefaultConfig("users")
|
||||
self.initlizeConfigToWidget("system", self.__system_config_data)
|
||||
self.initlizeConfigToWidget("users", self.__users_config_data)
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initilizeUserInfoWidget()
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
|
||||
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
|
||||
self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
|
||||
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
|
||||
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
|
||||
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
|
||||
self.BrowseCurrentSystemConfigButton.clicked.connect(self.onBrowseCurrentSystemConfigButtonClicked)
|
||||
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
|
||||
self.BrowseExportSystemConfigButton.clicked.connect(self.onBrowseExportSystemConfigButtonClicked)
|
||||
self.BrowseExportUserConfigButton.clicked.connect(self.onBrowseExportUserConfigButtonClicked)
|
||||
self.ExportConfigButton.clicked.connect(self.onExportConfigButtonClicked)
|
||||
self.NewConfigButton.clicked.connect(self.onNewConfigButtonClicked)
|
||||
self.LoadConfigButton.clicked.connect(self.onLoadConfigButtonClicked)
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.configWidgetCloseSingal.emit(self.__config_paths)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def initlizeFloorRoomMap(
|
||||
self
|
||||
):
|
||||
|
||||
self.__floor_map = {
|
||||
"2": "二层",
|
||||
"3": "三层",
|
||||
"4": "四层",
|
||||
"5": "五层"
|
||||
}
|
||||
self.__room_map = {
|
||||
"1": "二层内环",
|
||||
"2": "二层外环",
|
||||
"3": "三层内环",
|
||||
"4": "三层外环",
|
||||
"5": "四层内环",
|
||||
"6": "四层外环",
|
||||
"7": "四层期刊区",
|
||||
"8": "五层考研"
|
||||
}
|
||||
self.__floor_rmap = {
|
||||
v: k for k, v in self.__floor_map.items()
|
||||
}
|
||||
self.__room_rmap = {
|
||||
v: k for k, v in self.__room_map.items()
|
||||
}
|
||||
self.__floor_room_map = {
|
||||
"二层": ["二层内环", "二层外环"],
|
||||
"三层": ["三层内环", "三层外环"],
|
||||
"四层": ["四层内环", "四层外环", "四层期刊区"],
|
||||
"五层": ["五层考研"]
|
||||
}
|
||||
|
||||
|
||||
def initlizeDefaultConfigPaths(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
return {
|
||||
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
|
||||
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json"))
|
||||
}
|
||||
|
||||
|
||||
def initlizeDefaultConfig(
|
||||
self,
|
||||
which: str
|
||||
):
|
||||
|
||||
default_config_paths = self.initlizeDefaultConfigPaths()
|
||||
if which == "system":
|
||||
self.__system_config_data = self.defaultSystemConfig()
|
||||
self.__config_paths["system"] = default_config_paths["system"]
|
||||
self.saveSystemConfig(self.__config_paths["system"], self.__system_config_data)
|
||||
elif which == "users":
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
self.__config_paths["users"] = default_config_paths["users"]
|
||||
self.saveUsersConfig(self.__config_paths["users"], self.__users_config_data)
|
||||
if which == "system":
|
||||
file_type = "系统配置文件"
|
||||
elif which == "users":
|
||||
file_type = "用户配置文件"
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
f"{file_type}已初始化, \n"\
|
||||
f" 文件路径: {self.__config_paths[which]}"
|
||||
)
|
||||
|
||||
|
||||
def initlizeConfigToWidget(
|
||||
self,
|
||||
which: str,
|
||||
config_data: dict
|
||||
):
|
||||
|
||||
if which == "system":
|
||||
self.setSystemConfigToWidget(config_data)
|
||||
self.CurrentSystemConfigEdit.setText(self.__config_paths["system"])
|
||||
elif which == "users":
|
||||
self.initilizeUserInfoWidget()
|
||||
self.fillUsersList(config_data)
|
||||
self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
|
||||
|
||||
|
||||
def defaultSystemConfig(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"library": {
|
||||
"host_url": "http://10.1.20.7",
|
||||
"login_url": "/login"
|
||||
},
|
||||
"login": {
|
||||
"auto_captcha": True,
|
||||
"max_attempt": 3
|
||||
},
|
||||
"web_driver": {
|
||||
"driver_type": "edge",
|
||||
"driver_path": "msedgedriver.exe",
|
||||
"headless": False
|
||||
},
|
||||
"mode": {
|
||||
"run_mode": 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def defaultUsersConfig(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"users": []
|
||||
}
|
||||
|
||||
|
||||
def collectSystemConfigFromWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
system_config = self.defaultSystemConfig()
|
||||
# library config is never changed
|
||||
system_config["login"]["auto_captcha"] = self.AutoCaptchaCheckBox.isChecked()
|
||||
system_config["login"]["max_attempt"] = self.LoginAttemptSpinBox.value()
|
||||
system_config["web_driver"]["driver_type"] = self.BrowserTypeComboBox.currentText()
|
||||
system_config["web_driver"]["driver_path"] = self.BrowseBrowserDriverEdit.text()
|
||||
system_config["web_driver"]["headless"] = self.HeadlessCheckBox.isChecked()
|
||||
run_mode = 0
|
||||
if self.AutoReserveCheckBox.isChecked():
|
||||
run_mode |= 0x01
|
||||
if self.AutoCheckinCheckBox.isChecked():
|
||||
run_mode |= 0x02
|
||||
if self.AutoRenewalCheckBox.isChecked():
|
||||
run_mode |= 0x04
|
||||
system_config["mode"]["run_mode"] = run_mode
|
||||
return system_config
|
||||
|
||||
|
||||
def setSystemConfigToWidget(
|
||||
self,
|
||||
system_config: dict
|
||||
):
|
||||
|
||||
self.HostUrlEdit.setText(system_config["library"]["host_url"])
|
||||
self.LoginUrlEdit.setText(system_config["library"]["login_url"])
|
||||
self.AutoCaptchaCheckBox.setChecked(system_config["login"]["auto_captcha"])
|
||||
self.LoginAttemptSpinBox.setValue(system_config["login"]["max_attempt"])
|
||||
self.BrowserTypeComboBox.setCurrentText(system_config["web_driver"]["driver_type"])
|
||||
driver_path = os.path.abspath(system_config["web_driver"]["driver_path"])
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
|
||||
self.HeadlessCheckBox.setChecked(system_config["web_driver"]["headless"])
|
||||
run_mode = system_config["mode"]["run_mode"]
|
||||
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
|
||||
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
|
||||
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
|
||||
|
||||
|
||||
def initilizeUserInfoWidget(
|
||||
self
|
||||
):
|
||||
|
||||
self.UsernameEdit.setText("")
|
||||
self.PasswordEdit.setText("")
|
||||
self.UserListWidget.setSortingEnabled(True)
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Password)
|
||||
self.ShowPasswordCheckBox.setChecked(False)
|
||||
self.FloorComboBox.setCurrentIndex(1) # use for the '__init__' to effect the signal
|
||||
self.FloorComboBox.setCurrentIndex(0)
|
||||
self.DateEdit.setDate(QDate.currentDate())
|
||||
self.DateEdit.setMinimumDate(QDate.currentDate())
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate())
|
||||
if QTime.currentTime() > QTime(19, 0, 0) and QTime.currentTime() < QTime(23, 0, 0):
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate().addDays(1))
|
||||
self.BeginTimeEdit.setTime(QTime.currentTime())
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(False)
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(10)
|
||||
self.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
|
||||
self.PreferLateEndTimeCheckBox.setChecked(False)
|
||||
self.MaxEndTimeDiffSpinBox.setValue(10)
|
||||
self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
|
||||
self.SatisfyDurationCheckBox.setChecked(False)
|
||||
|
||||
|
||||
def collectUserConfigFromUserInfoWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
user_config = {
|
||||
"username": self.UsernameEdit.text(),
|
||||
"password": self.PasswordEdit.text(),
|
||||
"reserve_info": {
|
||||
"begin_time":{},
|
||||
"end_time": {}
|
||||
}
|
||||
}
|
||||
user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd")
|
||||
user_config["reserve_info"]["place"] = self.PlaceComboBox.currentText()
|
||||
user_config["reserve_info"]["floor"] = self.__floor_rmap[self.FloorComboBox.currentText()]
|
||||
user_config["reserve_info"]["room"] = self.__room_rmap[self.RoomComboBox.currentText()]
|
||||
user_config["reserve_info"]["seat_id"] = self.SeatIDEdit.text()
|
||||
user_config["reserve_info"]["begin_time"]["time"] = self.BeginTimeEdit.time().toString("HH:mm")
|
||||
user_config["reserve_info"]["begin_time"]["max_diff"] = self.MaxBeginTimeDiffSpinBox.value()
|
||||
user_config["reserve_info"]["begin_time"]["prefer_early"] = self.PreferEarlyBeginTimeCheckBox.isChecked()
|
||||
user_config["reserve_info"]["end_time"]["time"] = self.EndTimeEdit.time().toString("HH:mm")
|
||||
user_config["reserve_info"]["end_time"]["max_diff"] = self.MaxEndTimeDiffSpinBox.value()
|
||||
user_config["reserve_info"]["end_time"]["prefer_early"] = not self.PreferLateEndTimeCheckBox.isChecked()
|
||||
user_config["reserve_info"]["expect_duration"] = self.ExpectDurationSpinBox.value()
|
||||
user_config["reserve_info"]["satisfy_duration"] = self.SatisfyDurationCheckBox.isChecked()
|
||||
return user_config
|
||||
|
||||
|
||||
def collectUserConfigFromUserListWidget(
|
||||
self,
|
||||
index: int
|
||||
) -> dict:
|
||||
|
||||
user_config = self.defaultUsersConfig()
|
||||
if index < 0 or index >= self.UserListWidget.count():
|
||||
return user_config
|
||||
user_item = self.UserListWidget.item(index)
|
||||
if user_item:
|
||||
user_config = user_item.data(Qt.UserRole)
|
||||
return user_config
|
||||
|
||||
|
||||
def setUserConfigToWidget(
|
||||
self,
|
||||
user_config: dict
|
||||
) -> None:
|
||||
|
||||
try:
|
||||
self.UsernameEdit.setText(user_config["username"])
|
||||
self.PasswordEdit.setText(user_config["password"])
|
||||
self.DateEdit.setDate(QDate.fromString(user_config["reserve_info"]["date"], "yyyy-MM-dd"))
|
||||
self.PlaceComboBox.setCurrentText(user_config["reserve_info"]["place"])
|
||||
self.FloorComboBox.setCurrentText(self.__floor_map[user_config["reserve_info"]["floor"]])
|
||||
self.RoomComboBox.setCurrentText(self.__room_map[user_config["reserve_info"]["room"]])
|
||||
self.SeatIDEdit.setText(user_config["reserve_info"]["seat_id"])
|
||||
self.BeginTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["begin_time"]["time"], "H:mm"))
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(user_config["reserve_info"]["begin_time"]["max_diff"])
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(user_config["reserve_info"]["begin_time"]["prefer_early"])
|
||||
self.EndTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["end_time"]["time"], "H:mm"))
|
||||
self.MaxEndTimeDiffSpinBox.setValue(user_config["reserve_info"]["end_time"]["max_diff"])
|
||||
self.PreferLateEndTimeCheckBox.setChecked(not user_config["reserve_info"]["end_time"]["prefer_early"])
|
||||
self.ExpectDurationSpinBox.setValue(user_config["reserve_info"]["expect_duration"])
|
||||
self.SatisfyDurationCheckBox.setChecked(user_config["reserve_info"]["satisfy_duration"])
|
||||
except:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"用户配置文件读取发生错误 !\n"\
|
||||
f"用户: {user_config['username']} 配置文件可能已损坏"
|
||||
)
|
||||
|
||||
|
||||
def loadSystemConfig(
|
||||
self,
|
||||
system_config_path: str
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
if not system_config_path or not os.path.exists(system_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
system_config = ConfigReader(system_config_path).getConfigs()
|
||||
if system_config and "library" in system_config\
|
||||
and "web_driver" in system_config\
|
||||
and "login" in system_config:
|
||||
return system_config
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"系统配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {system_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveSystemConfig(
|
||||
self,
|
||||
system_config_path: str,
|
||||
system_config_data: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not system_config_path:
|
||||
raise Exception("文件路径为空")
|
||||
if not system_config_data or not isinstance(system_config_data, dict):
|
||||
raise Exception("系统配置数据为空或类型错误")
|
||||
ConfigWriter(system_config_path, system_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: {system_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def loadUsersConfig(
|
||||
self,
|
||||
users_config_path: str
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
if not users_config_path or not os.path.exists(users_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
users_config = ConfigReader(users_config_path).getConfigs()
|
||||
if users_config and "users" in users_config:
|
||||
return users_config
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {users_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveUsersConfig(
|
||||
self,
|
||||
users_config_path: str,
|
||||
users_config_data: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not users_config_path:
|
||||
raise Exception("文件路径为空")
|
||||
if not users_config_data or not isinstance(users_config_data, dict):
|
||||
raise Exception("用户配置数据为空或类型错误")
|
||||
ConfigWriter(users_config_path, users_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: \n{users_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def saveConfigs(
|
||||
self,
|
||||
system_config_path: str,
|
||||
users_config_path: str
|
||||
) -> bool:
|
||||
|
||||
if users_config_path:
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
for index in range(self.UserListWidget.count()):
|
||||
user_config = self.collectUserConfigFromUserListWidget(index)
|
||||
if user_config:
|
||||
self.__users_config_data["users"].append(user_config)
|
||||
if not self.saveUsersConfig(
|
||||
users_config_path,
|
||||
self.__users_config_data
|
||||
):
|
||||
return False
|
||||
if system_config_path:
|
||||
self.__system_config_data = self.collectSystemConfigFromWidget()
|
||||
if not self.saveSystemConfig(
|
||||
system_config_path,
|
||||
self.__system_config_data
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def loadConfig(
|
||||
self,
|
||||
config_path: str
|
||||
) -> bool:
|
||||
|
||||
if not config_path:
|
||||
config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择配置文件 - AutoLibrary",
|
||||
f"{QDir.toNativeSeparators(QDir.currentPath())}",
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if not config_path:
|
||||
return False
|
||||
try:
|
||||
system_config = self.loadSystemConfig(config_path)
|
||||
users_config = self.loadUsersConfig(config_path)
|
||||
if system_config is not None:
|
||||
self.__config_paths["system"] = config_path
|
||||
self.__system_config_data.update(system_config)
|
||||
self.setSystemConfigToWidget(self.__system_config_data)
|
||||
self.CurrentSystemConfigEdit.setText(config_path)
|
||||
return True
|
||||
if users_config is not None:
|
||||
self.__config_paths["users"] = config_path
|
||||
self.__users_config_data.update(users_config)
|
||||
self.fillUsersList(self.__users_config_data)
|
||||
self.CurrentUserConfigEdit.setText(config_path)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def fillUsersList(
|
||||
self,
|
||||
users_config_data: list[dict]
|
||||
):
|
||||
|
||||
self.UserListWidget.clear()
|
||||
if "users" in users_config_data:
|
||||
for user in users_config_data["users"]:
|
||||
user_item = QListWidgetItem(user["username"])
|
||||
user_item.setData(Qt.UserRole, user)
|
||||
self.UserListWidget.addItem(user_item)
|
||||
|
||||
|
||||
def addUser(
|
||||
self
|
||||
):
|
||||
|
||||
new_user = {
|
||||
"username": f"新用户-{self.UserListWidget.count()}",
|
||||
"password": "000000",
|
||||
"reserve_info": {
|
||||
"date": f"{QDate.currentDate().toString("yyyy-MM-dd")}",
|
||||
"place": "\u56fe\u4e66\u9986",
|
||||
"floor": "2",
|
||||
"room": "1",
|
||||
"seat_id": "",
|
||||
"begin_time": {
|
||||
"time": f"{QTime.currentTime().toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"prefer_early": False
|
||||
},
|
||||
"end_time": {
|
||||
"time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"prefer_early": True
|
||||
},
|
||||
"expect_duration": 2.0,
|
||||
"satisfy_duration": False
|
||||
}
|
||||
}
|
||||
user_item = QListWidgetItem(new_user["username"])
|
||||
user_item.setData(Qt.UserRole, new_user)
|
||||
self.UserListWidget.addItem(user_item)
|
||||
self.UserListWidget.setCurrentItem(user_item)
|
||||
self.setUserConfigToWidget(new_user)
|
||||
|
||||
|
||||
def delUser(
|
||||
self
|
||||
):
|
||||
|
||||
current_item = self.UserListWidget.currentItem()
|
||||
if current_item:
|
||||
self.UserListWidget.takeItem(self.UserListWidget.row(current_item))
|
||||
self.UserListWidget.setCurrentItem(None)
|
||||
|
||||
@Slot()
|
||||
def onShowPasswordCheckBoxChecked(
|
||||
self,
|
||||
checked: bool
|
||||
):
|
||||
|
||||
if checked:
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Normal)
|
||||
else:
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Password)
|
||||
|
||||
@Slot()
|
||||
def onFloorComboBoxCurrentIndexChanged(
|
||||
self,
|
||||
):
|
||||
|
||||
floor = self.FloorComboBox.currentText()
|
||||
self.RoomComboBox.clear()
|
||||
self.RoomComboBox.addItems(self.__floor_room_map[floor])
|
||||
self.RoomComboBox.setCurrentIndex(0)
|
||||
|
||||
@Slot()
|
||||
def onUserListWidgetCurrentItemChanged(
|
||||
self,
|
||||
current: QListWidgetItem,
|
||||
previous: QListWidgetItem
|
||||
):
|
||||
# dont care about the 'self.__users_config_data', we already
|
||||
# cant effectively update the data of each user, due to the
|
||||
# possiblity of frequency edit. we just let the QListWidget
|
||||
# help us.
|
||||
if not current:
|
||||
self.initilizeUserInfoWidget()
|
||||
return
|
||||
if previous:
|
||||
user = self.collectUserConfigFromUserInfoWidget()
|
||||
if user:
|
||||
previous.setText(user["username"])
|
||||
previous.setData(Qt.UserRole, user)
|
||||
user = current.data(Qt.UserRole)
|
||||
if user:
|
||||
self.setUserConfigToWidget(user)
|
||||
|
||||
@Slot()
|
||||
def onAddUserButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.addUser()
|
||||
|
||||
@Slot()
|
||||
def onDelUserButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.delUser()
|
||||
|
||||
@Slot()
|
||||
def onBrowseBrowserDriverButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
browser_driver_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择浏览器驱动 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"可执行文件 (*.exe);;所有文件 (*)"
|
||||
)[0]
|
||||
if browser_driver_path:
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
|
||||
|
||||
@Slot()
|
||||
def onBrowseCurrentSystemConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
system_config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择其它的系统配置 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if system_config_path:
|
||||
system_config_path = QDir.toNativeSeparators(system_config_path)
|
||||
self.loadConfig(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)
|
||||
self.loadConfig(users_config_path)
|
||||
|
||||
@Slot()
|
||||
def onBrowseExportSystemConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
system_config_path = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"导出系统配置 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if system_config_path:
|
||||
self.ExportSystemConfigEdit.setText(QDir.toNativeSeparators(system_config_path))
|
||||
|
||||
@Slot()
|
||||
def onBrowseExportUserConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
users_config_path = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"导出用户配置 - AutoLibrary",
|
||||
self.CurrentUserConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if users_config_path:
|
||||
self.ExportUserConfigEdit.setText(QDir.toNativeSeparators(users_config_path))
|
||||
|
||||
@Slot()
|
||||
def onExportConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
msg = ""
|
||||
|
||||
system_config_path = self.ExportSystemConfigEdit.text()
|
||||
users_config_path = self.ExportUserConfigEdit.text()
|
||||
if system_config_path:
|
||||
if self.saveConfigs(
|
||||
system_config_path,
|
||||
users_config_path=""
|
||||
):
|
||||
msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
|
||||
if users_config_path:
|
||||
if self.saveConfigs(
|
||||
"", users_config_path
|
||||
):
|
||||
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
|
||||
if msg:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
msg
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onLoadConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.loadConfig("")
|
||||
|
||||
@Slot()
|
||||
def onNewConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
file_path = self.CurrentSystemConfigEdit.text()
|
||||
folder_dir = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"选择新建配置的文件夹 - AutoLibrary",
|
||||
QDir.toNativeSeparators(QFileInfo(os.path.abspath(file_path)).absoluteDir().path())
|
||||
)
|
||||
if not folder_dir:
|
||||
return
|
||||
system_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "system.json"))
|
||||
users_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "users.json"))
|
||||
system_exists = os.path.isfile(system_config_path)
|
||||
users_exists = os.path.isfile(users_config_path)
|
||||
if system_exists or users_exists:
|
||||
exist_files = []
|
||||
if system_exists:
|
||||
exist_files.append(system_config_path)
|
||||
if users_exists:
|
||||
exist_files.append(users_config_path)
|
||||
reply = QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
f"文件夹中已存在以下文件, 是否覆盖 ?\n{chr(10).join(exist_files)}",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.No:
|
||||
return
|
||||
self.__system_config_data = self.defaultSystemConfig()
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
self.__config_paths = {
|
||||
"system": system_config_path,
|
||||
"users": users_config_path
|
||||
}
|
||||
self.initlizeConfigToWidget("system", self.__system_config_data)
|
||||
self.initlizeConfigToWidget("users", self.__users_config_data)
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.UserListWidget.currentItem() is not None:
|
||||
user = self.collectUserConfigFromUserInfoWidget()
|
||||
if user:
|
||||
self.UserListWidget.currentItem().setData(Qt.UserRole, user)
|
||||
if self.saveConfigs(
|
||||
self.__config_paths["system"],
|
||||
self.__config_paths["users"]
|
||||
):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
"配置文件保存成功 !\n"
|
||||
f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
|
||||
f"用户配置文件路径: \n{self.__config_paths['users']}"
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"配置文件保存失败, 请检查文件路径权限"
|
||||
)
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.close()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QMenu
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QTextCursor, QCloseEvent, QFont, QIcon
|
||||
)
|
||||
|
||||
from .Ui_ALMainWindow import Ui_ALMainWindow
|
||||
from .ALConfigWidget import ALConfigWidget
|
||||
|
||||
from . import AutoLibraryResource
|
||||
|
||||
from AutoLib import AutoLib
|
||||
from ConfigReader import ConfigReader
|
||||
|
||||
|
||||
class AutoLibWorker(QThread):
|
||||
|
||||
finishedSignal = Signal()
|
||||
showTraceSignal = Signal(str)
|
||||
showMsgSignal = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.__input_queue = input_queue
|
||||
self.__output_queue = output_queue
|
||||
self.__config_paths = config_paths
|
||||
self.__stopped = False
|
||||
|
||||
|
||||
def checkTimeAvailable(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
current_time = time.strftime("%H:%M", time.localtime())
|
||||
if current_time >= "23:30" or current_time <= "07:30":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def checkConfigPaths(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
if not all(
|
||||
os.path.exists(path) for path in self.__config_paths.values()
|
||||
):
|
||||
self.showTraceSignal.emit(
|
||||
"配置文件路径不存在,请检查配置文件路径是否正确。"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
|
||||
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"]),
|
||||
)
|
||||
auto_lib.close()
|
||||
self.showTraceSignal.emit("AutoLibrary 运行结束")
|
||||
except Exception as e:
|
||||
self.showTraceSignal.emit(
|
||||
f"AutoLibrary 运行时发生异常:{e}"
|
||||
)
|
||||
finally:
|
||||
self.finishedSignal.emit()
|
||||
|
||||
|
||||
def stop(
|
||||
self
|
||||
):
|
||||
|
||||
self.__stopped = True
|
||||
|
||||
|
||||
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.__class_name = self.__class__.__name__
|
||||
|
||||
self.setupUi(self)
|
||||
self.__input_queue = queue.Queue()
|
||||
self.__output_queue = queue.Queue()
|
||||
self.__auto_lib = AutoLib(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
)
|
||||
self.__config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
"users":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
|
||||
}
|
||||
self.__alConfigWidget = None
|
||||
self.__auto_lib_thread = None
|
||||
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.startMsgPolling()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
|
||||
self.setWindowIcon(icon)
|
||||
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
|
||||
self.StartButton.clicked.connect(self.onStartButtonClicked)
|
||||
self.StopButton.clicked.connect(self.onStopButtonClicked)
|
||||
self.SendButton.clicked.connect(self.onSendButtonClicked)
|
||||
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent,
|
||||
):
|
||||
|
||||
if self.__timer and self.__timer.isActive():
|
||||
self.__timer.stop()
|
||||
if self.__auto_lib:
|
||||
self.__auto_lib.close()
|
||||
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,
|
||||
):
|
||||
|
||||
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)
|
||||
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 and self.__auto_lib_thread.isRunning():
|
||||
self.__auto_lib_thread.stop()
|
||||
self.showTrace("正在停止操作......")
|
||||
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,264 @@
|
||||
<?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="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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 785 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
This folder is used to store the models by ddddocr.
|
||||
Reference in New Issue
Block a user