Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd565ec57d | |||
| 9f17474c1b | |||
| 04d66346dc | |||
| f858295af1 | |||
| cd6c899388 | |||
| 1038a86aff | |||
| 15ea47dd07 | |||
| 829a8440ad | |||
| 389ac885d3 | |||
| 68b61b5c8c | |||
| fd5abb5f1e |
@@ -21,6 +21,7 @@ from LibChecker import LibChecker
|
||||
from LibLogin import LibLogin
|
||||
from LibLogout import LibLogout
|
||||
from LibReserve import LibReserve
|
||||
from LibCheckin import LibCheckin
|
||||
|
||||
from ConfigReader import ConfigReader
|
||||
|
||||
@@ -90,7 +91,7 @@ class AutoLib(MsgBase):
|
||||
self.__driver = webdriver.Firefox(service=service, options=edge_options)
|
||||
case _:
|
||||
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
|
||||
self.__driver.implicitly_wait(10)
|
||||
self.__driver.implicitly_wait(1)
|
||||
self.__driver.execute_script(
|
||||
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
)
|
||||
@@ -112,6 +113,7 @@ class AutoLib(MsgBase):
|
||||
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver)
|
||||
|
||||
|
||||
def __waitResponseLoad(
|
||||
@@ -120,7 +122,7 @@ class AutoLib(MsgBase):
|
||||
|
||||
# wait for page load
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until( # title contains "首页"
|
||||
WebDriverWait(self.__driver, 2).until( # title contains "首页"
|
||||
EC.title_contains("首页")
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # username field presence
|
||||
@@ -145,7 +147,9 @@ class AutoLib(MsgBase):
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
url = self.__system_config_reader.get("library/host_url")
|
||||
url += self.__system_config_reader.get("library/login_url")
|
||||
self.__driver.get(url)
|
||||
if not self.__waitResponseLoad():
|
||||
return False
|
||||
return True
|
||||
@@ -159,7 +163,7 @@ class AutoLib(MsgBase):
|
||||
) -> int:
|
||||
|
||||
# result : 0 - success, 1 - failed, 2 - passed
|
||||
result = 1
|
||||
result = 2
|
||||
|
||||
# login
|
||||
if not self.__lib_login.login(
|
||||
@@ -188,6 +192,26 @@ class AutoLib(MsgBase):
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法预约,已跳过")
|
||||
result = 2
|
||||
# checkin
|
||||
if run_mode["auto_checkin"] and result == 2:
|
||||
if self.__lib_checker.canCheckin(reserve_info.get("date")):
|
||||
if self.__lib_checkin.checkin(username):
|
||||
self._showTrace(f"用户 {username} 签到成功 !")
|
||||
result = 0
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 签到失败 !")
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法签到,已跳过")
|
||||
result = 2
|
||||
# renewal
|
||||
if run_mode["auto_renewal"] and result == 2:
|
||||
if self.__lib_checker.canRenew(reserve_info.get("date")):
|
||||
pass
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法续约,已跳过")
|
||||
result = 2
|
||||
# logout
|
||||
if not self.__lib_logout.logout(
|
||||
|
||||
@@ -44,9 +44,9 @@ class LibChecker(LibOperator):
|
||||
seconds: float
|
||||
) -> str:
|
||||
|
||||
hours = int(seconds // 3600)
|
||||
minutes = int(seconds % 3600 // 60)
|
||||
seconds = int(seconds % 60)
|
||||
hours = int(seconds//3600)
|
||||
minutes = int(seconds%3600//60)
|
||||
seconds = int(seconds%60)
|
||||
return f"{hours} 时 {minutes} 分 {seconds} 秒"
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class LibChecker(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
@@ -67,6 +67,44 @@ class LibChecker(LibOperator):
|
||||
return True
|
||||
|
||||
|
||||
def __decodeReserveTime(
|
||||
self,
|
||||
time_element
|
||||
) -> dict:
|
||||
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
|
||||
if date_match:
|
||||
date = date_match.group(1)
|
||||
else:
|
||||
date = ""
|
||||
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
|
||||
if time_match:
|
||||
begin_time = time_match.group(1)
|
||||
end_time = time_match.group(2)
|
||||
else:
|
||||
begin_time = ""
|
||||
end_time = ""
|
||||
return {
|
||||
"date": date,
|
||||
"time": {
|
||||
"begin": begin_time,
|
||||
"end": end_time
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveInfo(
|
||||
self,
|
||||
info_elements
|
||||
@@ -108,42 +146,80 @@ class LibChecker(LibOperator):
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
except:
|
||||
return None
|
||||
# process time element to get the date string
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
|
||||
if date_match:
|
||||
date_str = date_match.group(1)
|
||||
else:
|
||||
date_str = ""
|
||||
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
|
||||
if time_match:
|
||||
begin_time = time_match.group(1)
|
||||
end_time = time_match.group(2)
|
||||
else:
|
||||
time_str = ""
|
||||
return {
|
||||
"date": "",
|
||||
"time": {"begin": "", "end": ""},
|
||||
"info": {"location": "", "status": ""}
|
||||
}
|
||||
time = self.__decodeReserveTime(time_element)
|
||||
info = self.__decodeReserveInfo(info_elements)
|
||||
return {
|
||||
"date": date_str,
|
||||
"time": {
|
||||
"begin": begin_time,
|
||||
"end": end_time,
|
||||
},
|
||||
"date": time["date"],
|
||||
"time": time["time"],
|
||||
"info": info
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveRecords(
|
||||
self,
|
||||
reservations
|
||||
) -> list:
|
||||
|
||||
records = []
|
||||
|
||||
for reservation in reservations:
|
||||
record = self.__decodeReserveRecord(reservation)
|
||||
if record["date"] == "":
|
||||
record = None
|
||||
if record["time"] == {"begin": "", "end": ""}:
|
||||
record = None
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
def __loadReserveRecords(
|
||||
self
|
||||
) -> list:
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
|
||||
)
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
|
||||
)
|
||||
return reservations
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return None
|
||||
|
||||
|
||||
def __showMoreReserveRecords(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
WebDriverWait(self.__driver, 0.1).until(
|
||||
EC.element_to_be_clickable((By.ID, "moreBtn"))
|
||||
)
|
||||
except:
|
||||
# the reservation is the last one
|
||||
return False
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
return True
|
||||
else:
|
||||
self._showTrace("用户无法加载更多预约记录")
|
||||
return False
|
||||
except:
|
||||
self._showTrace("加载更多预约记录失败 !")
|
||||
return False
|
||||
|
||||
|
||||
def __getReserveRecord(
|
||||
self,
|
||||
wanted_date: str,
|
||||
@@ -154,7 +230,6 @@ class LibChecker(LibOperator):
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态")
|
||||
return None
|
||||
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
|
||||
date_obj = datetime.strptime(wanted_date, "%Y-%m-%d").date()
|
||||
|
||||
checked_count = 0
|
||||
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
|
||||
@@ -162,59 +237,30 @@ class LibChecker(LibOperator):
|
||||
if not self.__navigateToReserveRecordPage():
|
||||
return None
|
||||
for _ in range(max_check_times):
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
|
||||
)
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
reservations = self.__loadReserveRecords()
|
||||
if reservations is None:
|
||||
return None
|
||||
for i in range(checked_count, len(reservations)): # the last one is load button
|
||||
reservation = reservations[i]
|
||||
record = self.__decodeReserveRecord(reservation)
|
||||
records = self.__decodeReserveRecords(reservations[checked_count:])
|
||||
for record in records:
|
||||
checked_count += 1
|
||||
if record is None:
|
||||
continue
|
||||
record_date = record["date"]
|
||||
record_time = record["time"]
|
||||
status = record["info"]["status"]
|
||||
location = record["info"]["location"]
|
||||
if record_date == "" or record_time == {"begin": "", "end": ""}:
|
||||
# record date is later than the given date, check the next one
|
||||
if datetime.strptime(record["date"], "%Y-%m-%d").date() >\
|
||||
datetime.strptime(wanted_date, "%Y-%m-%d").date():
|
||||
continue
|
||||
is_wanted = (status == wanted_status)
|
||||
# reservation is later than the given date, check the next one
|
||||
if datetime.strptime(record_date, "%Y-%m-%d").date() > date_obj:
|
||||
continue
|
||||
# reservation is earlier than the given date, can reserve
|
||||
if datetime.strptime(record_date, "%Y-%m-%d").date() < date_obj:
|
||||
# record date is earlier than the given date, so there is no wanted record
|
||||
if datetime.strptime(record["date"], "%Y-%m-%d").date() <\
|
||||
datetime.strptime(wanted_date, "%Y-%m-%d").date():
|
||||
return None
|
||||
# query the wanted status
|
||||
if is_wanted:
|
||||
if record["info"]["status"] == wanted_status:
|
||||
self._showTrace(
|
||||
f"寻找到用户第 {i + 1} 条状态为 {wanted_status} 的预约记录, "
|
||||
f"详细信息: {record_date} {record_time['begin']} - {record_time['end']} {location}"
|
||||
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
|
||||
f"详细信息: {record["date"]} "
|
||||
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
|
||||
)
|
||||
return {
|
||||
"index": i,
|
||||
"date": record_date,
|
||||
"time": record_time,
|
||||
"status": wanted_status
|
||||
}
|
||||
checked_count = len(reservations)
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
else:
|
||||
self._showTrace("用户无法加载更多预约记录")
|
||||
break
|
||||
except:
|
||||
self._showTrace("加载更多预约记录失败 !")
|
||||
return record
|
||||
if not self.__showMoreReserveRecords():
|
||||
break
|
||||
return None
|
||||
|
||||
@@ -252,21 +298,21 @@ class LibChecker(LibOperator):
|
||||
if time_diff_seconds < -30*60:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
|
||||
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
|
||||
)
|
||||
return False
|
||||
# before in 30 minutes, can checkin
|
||||
elif -30*60 <= time_diff_seconds < 0:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
# past less than 30 minutes, can checkin
|
||||
elif 0 <= time_diff_seconds < 30*60:
|
||||
elif 0 <= time_diff_seconds < 30*60 - 5: # spare 5 seconds for the checkin process
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前时间已经 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
f"当前距离预约开始时间已经过去 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
|
||||
@@ -286,17 +332,15 @@ class LibChecker(LibOperator):
|
||||
time_diff = end_time - datetime.now()
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# a using record is definitely after the begin time
|
||||
trace_msg = (
|
||||
f"用户在 {date} 的预约结束时间为 {end_time}, "
|
||||
f"当前距离预约结束时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}"
|
||||
)
|
||||
if abs(time_diff_seconds) < 120*60:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约结束时间为 {end_time}, "
|
||||
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以续约"
|
||||
)
|
||||
self._showTrace(f"{trace_msg}, 可以续约")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约结束时间为 {end_time}, "
|
||||
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法续约"
|
||||
)
|
||||
self._showTrace(f"{trace_msg}, 无法续约")
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
|
||||
return False
|
||||
|
||||
@@ -34,7 +34,74 @@ class LibCheckin(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "resultMessage"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.CLASS_NAME, "btnOK"))
|
||||
)
|
||||
result_message_element = self.__driver.find_element(
|
||||
By.CLASS_NAME, "resultMessage"
|
||||
)
|
||||
ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK")
|
||||
except:
|
||||
self._showTrace("签到时发生未知错误 !")
|
||||
return False
|
||||
print(result_message_element)
|
||||
result_message = result_message_element.text
|
||||
if "签到成功" in result_message:
|
||||
try:
|
||||
detail_elements = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".resultMessage dd"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
if detail_elements:
|
||||
details = [element.text for element in detail_elements if element.text.strip()]
|
||||
if len(details) >= 5:
|
||||
self._showTrace(f"\n"\
|
||||
f" 签到成功 !\n"\
|
||||
f" {details[1]}\n"\
|
||||
f" {details[2]}\n"\
|
||||
f" {details[3]}\n"\
|
||||
f" {details[4]}")
|
||||
else:
|
||||
self._showTrace(
|
||||
" 签到成功 !\n"\
|
||||
" 未获取到签到详情 !")
|
||||
ok_btn.click()
|
||||
return True
|
||||
else:
|
||||
failure_reason = result_message.replace("签到失败", "").strip()
|
||||
self._showTrace(f"签到失败: {failure_reason}")
|
||||
ok_btn.click()
|
||||
return False
|
||||
|
||||
|
||||
def checkin(
|
||||
self,
|
||||
username: str
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
return False
|
||||
try:
|
||||
checkin_btn = WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "btnCheckIn"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"用户 {username} 签到界面加载失败 !")
|
||||
return False
|
||||
if "disabled" in checkin_btn.get_attribute("class"):
|
||||
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
|
||||
return False
|
||||
checkin_btn.click()
|
||||
return self._waitResponseLoad()
|
||||
|
||||
@@ -41,13 +41,13 @@ class LibLogin(LibOperator):
|
||||
|
||||
# wait to verify login success
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until( # title contains "自选座位 :: 座位预约系统"
|
||||
WebDriverWait(self.__driver, 2).until( # title contains "自选座位 :: 座位预约系统"
|
||||
EC.title_contains("自选座位 :: 座位预约系统")
|
||||
)
|
||||
WebDriverWait(self.__driver, 3).until( # search button presence
|
||||
WebDriverWait(self.__driver, 2).until( # search button presence
|
||||
EC.presence_of_element_located((By.ID, "search"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 3).until( # select content presence
|
||||
WebDriverWait(self.__driver, 2).until( # select content presence
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "selectContent"))
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -55,13 +55,13 @@ class LibReserve(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "layoutSeat"))
|
||||
)
|
||||
title_elements = []
|
||||
# reserve failed without title elements, so we need to try
|
||||
try:
|
||||
WebDriverWait(self.__driver, 1).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".layoutSeat dt"))
|
||||
)
|
||||
title_elements = self.__driver.find_elements(
|
||||
@@ -83,16 +83,12 @@ class LibReserve(LibOperator):
|
||||
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}")
|
||||
f" {contents[1]}\n"\
|
||||
f" {contents[2]}\n"\
|
||||
f" {contents[3]}\n"\
|
||||
f" 签到时间 :{contents[5]}")
|
||||
else:
|
||||
self._showTrace(f"\n"\
|
||||
f" 预约成功 !\n"\
|
||||
@@ -313,12 +309,12 @@ class LibReserve(LibOperator):
|
||||
|
||||
try:
|
||||
# click the trigger element
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(trigger_locator)
|
||||
).click()
|
||||
if option_locator:
|
||||
# select the option element if specified
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(option_locator)
|
||||
).click()
|
||||
self._showTrace(success_msg)
|
||||
@@ -390,9 +386,16 @@ class LibReserve(LibOperator):
|
||||
|
||||
try:
|
||||
# wait fot seat layout element to load
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"座位加载失败 !")
|
||||
return False
|
||||
try:
|
||||
all_seats = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, "li[id^='seat_']"
|
||||
)
|
||||
@@ -401,7 +404,7 @@ class LibReserve(LibOperator):
|
||||
if not seat_id_upper == seat.text.lstrip('0'):
|
||||
continue
|
||||
seat_link = seat.find_element(By.TAG_NAME, "a")
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(seat_link)
|
||||
)
|
||||
seat_link.click()
|
||||
@@ -423,6 +426,15 @@ class LibReserve(LibOperator):
|
||||
prefer_earlier: bool = True
|
||||
) -> int:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_all_elements_located(
|
||||
(By.CSS_SELECTOR, f"#{time_id} ul li a")
|
||||
)
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
return -1
|
||||
try:
|
||||
all_time_opts = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR,
|
||||
@@ -433,6 +445,9 @@ class LibReserve(LibOperator):
|
||||
best_actual_diff = None
|
||||
best_time_opt = None
|
||||
|
||||
if not all_time_opts:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
return -1
|
||||
for time_opt in all_time_opts:
|
||||
time_attr = time_opt.get_attribute("time")
|
||||
if time_attr == "now":
|
||||
@@ -548,7 +563,7 @@ class LibReserve(LibOperator):
|
||||
return False
|
||||
# map page
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
|
||||
|
Before Width: | Height: | Size: 785 KiB |
@@ -1,964 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="./AutoLibrary.ico" type="image/x-icon">
|
||||
<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;
|
||||
}
|
||||
|
||||
.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-content ol {
|
||||
padding-left: 1em;
|
||||
}
|
||||
.step-content ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
.step-content li {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
background: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin: 1rem 0;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.step-image img {
|
||||
max-width: 60%;
|
||||
height: auto;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.intro-box {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid var(--secondary);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
.info {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #0783ff;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
.important {
|
||||
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;
|
||||
}
|
||||
.highlight {
|
||||
background: #e3f2fd;
|
||||
color: #3498db;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9rem;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.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: 15px 15px 5px 5px;
|
||||
font-size: 0.9rem;
|
||||
overflow-x: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.code-block .bool { color: #569CD6; }
|
||||
.code-block .string { color: #CE9178; }
|
||||
.code-block .number { color: #B5CEA8; }
|
||||
.code-block .boolean { color: #569CD6; }
|
||||
.code-block .null { color: #569CD6; }
|
||||
.code-block .property { color: #9CDCFE; }
|
||||
.code-block .punctuation { color: #D4D4D4; }
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.tab-button {
|
||||
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;
|
||||
}
|
||||
.tab-button.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;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.tab-pane {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
.tab-pane.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* align-items: center;
|
||||
justify-content: center; */
|
||||
}
|
||||
.tab-pane img {
|
||||
max-width: 60%;
|
||||
max-height: 60%;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.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;
|
||||
background: rgb(220, 220, 220);
|
||||
display: none;
|
||||
}
|
||||
.faq-item.active .faq-answer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.browser-drivers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
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>操作手册 alpha-v0.03</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="#features">功能介绍</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" style="display: flex; align-items: center; gap: 10px;">
|
||||
<img src="./AutoLibrary.ico" alt="AutoLibrary" style="width: 80px; height: 80px;">
|
||||
<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="info">
|
||||
<strong>提示:</strong> 浏览器驱动版本必须与您的浏览器版本兼容,否则本工具将无法正常工作。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>确认驱动路径</h3>
|
||||
<p>下载驱动后,将浏览器驱动程序的路径通过配置窗口加载到AutoLibrary中。</p>
|
||||
|
||||
<p>例如:<span class="highlight">C:\Users\Administrator\Downloads\msedgedriver.exe</span></p>
|
||||
<div class="step-image">
|
||||
<img src="./配置窗口-系统配置-浏览器路径选择.png" alt="浏览器驱动路径示意图">
|
||||
</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>双击运行AutoLibrary.exe文件,工具将启动主界面。</p>
|
||||
<div class="info">
|
||||
<strong>提示:</strong>软件首次启动,未初始化配置文件,直接运行脚本会提示失败。
|
||||
</div>
|
||||
<div class="step-image">
|
||||
<img src="./运行主界面.png" alt="运行主界面">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>配置工具</h3>
|
||||
<p>对于不同用户的需求,你可以使用两种不同的方式来配置工具</p>
|
||||
<p>1. 使用界面配置:点击主界面窗口右上角的配置按钮,打开配置窗口。</p>
|
||||
<div id="use-ui" class="tabs-container">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-button active" data-tab="user-config">用户配置</button>
|
||||
<button class="tab-button" data-tab="system-config">系统配置</button>
|
||||
<button class="tab-button" data-tab="other-config">其它</button>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div id="user-config" class="tab-pane active">
|
||||
<div class="step-image">
|
||||
<img src="./配置窗口-用户配置.png" alt="配置窗口-用户配置">
|
||||
</div>
|
||||
<div class="info">
|
||||
<strong>提示:</strong>初次运行软件时,用户配置默认为空,需要手动添加。
|
||||
</div>
|
||||
<h4>用户列表</h4>
|
||||
<p>用户列表显示当前配置文件中的所有用户,你可以添加、删除用户。选中用户项以进行详细的配置。</p>
|
||||
<h4>用户信息</h4>
|
||||
<ol>
|
||||
<p><i>-学号:</i>用户的学号。</p>
|
||||
<p><i>-密码:</i>用户的密码,用户默认密码为000000。</p>
|
||||
</ol>
|
||||
<h4>预约信息</h4>
|
||||
<ol>
|
||||
<p><i>-日期(YYYY-MM-DD):</i>座位预约日期,默认显示当前日期,无法更改(图书馆19:00-23:00可以预约第二天座位,软件将在18:00-23:00允许用户选择第二天的日期)。</p>
|
||||
<p><i>-地点:</i>预约座位的地点,默认值为“图书馆”。</p>
|
||||
<p><i>-楼层:</i>预约座位的楼层,默认值为“二层”。</p>
|
||||
<p><i>-区域:</i>预约座位的区域,默认值为“二层内环”。</p>
|
||||
<p><i>-座位号:</i>预约座位的座位号。</p>
|
||||
<p><i>-开始时间(HH:mm):</i>预约座位的开始时间,默认值为当前时间,可选时间范围为7:30-23:30。</p>
|
||||
<p><i>-结束时间(HH:mm):</i>预约座位的结束时间,默认值为当前时间加上两个小时,可选时间范围与开始时间相同。</p>
|
||||
<p><i>-最大时间偏差(分钟):</i>选择的开始/结束时间不可用时,会按照该时间偏差范围寻找最近的可用时间。选择0则表示严格按照选择的时间预约,可选范围为0-120分钟。</p>
|
||||
<p><i>-优先选择最早/晚:</i>当预约时间列表中存在多个相距最近的可用时间时,选择最早(开始时间)/最晚(结束时间)的时间,不勾选将会按照脚本默认行为选择。</p>
|
||||
<p><i>-期望时长(小时):</i>预约座位的期望时长,默认值为“2小时”,可选范围为0-8小时。</p>
|
||||
<p><i>-优先满足期望时长:</i>勾选此项,会优先满足预约时长限制,当座位紧张时可能会导致预约失败。</p>
|
||||
</ol>
|
||||
</div>
|
||||
<div id="system-config" class="tab-pane">
|
||||
<div class="step-image">
|
||||
<img src="./配置窗口-系统配置.png" alt="配置窗口-系统配置">
|
||||
</div>
|
||||
<h4>图书馆设置</h4>
|
||||
<p>这里主要包含了关于图书馆的访问网址设置,不需要更改。</p>
|
||||
<h4>浏览器设置</h4>
|
||||
<p>主要包含浏览器类别选择(当前支持Edge Chromium和Mozilla Firefox),浏览器驱动路径选择以及无头模式设置。</p>
|
||||
<ol>
|
||||
<p><i>-浏览器类别:</i>选择您使用的浏览器类别(Edge Chromium或Mozilla Firefox)。</p>
|
||||
<p><i>-浏览器驱动路径:</i>点击浏览按钮选择对应浏览器类型和版本的浏览器驱动程序的路径。</p>
|
||||
<p><i>-无头模式:</i>如果您不希望看到浏览器窗口自动操作,可将无头模式设置为true。</p>
|
||||
</ol>
|
||||
<h4>登录设置</h4>
|
||||
<ol>
|
||||
<p><i>-自动识别验证码:</i>默认勾选。</p>
|
||||
<p><i>-登录尝试次数:</i>设置登录尝试的最大次数,默认值为3次。</p>
|
||||
</ol>
|
||||
<h4>运行模式</h4>
|
||||
<ol>
|
||||
<p><i>-自动预约:</i>脚本按照配置中起始时间和预期时长进行预约,用户如果当天存在有效预约,将自动跳过预约步骤。</p>
|
||||
<p><i>-自动签到:</i>如果用户在脚本启动时满足图书馆预约条件,将自动签到,如果用户当天无有效预约或不在可签到时间内,则自动跳过。</p>
|
||||
<p><i>-自动续约:</i>如果用户在脚本启动时满足图书馆预约条件,将自动续约,如果用户当天无有效预约或不在可续约时间内,则自动跳过。</p>
|
||||
</ol>
|
||||
</div>
|
||||
<div id="other-config" class="tab-pane">
|
||||
<div class="step-image">
|
||||
<img src="./配置窗口-其它.png" alt="配置窗口-其它">
|
||||
</div>
|
||||
<h4>当前配置:</h4>
|
||||
<p>这里主要显示脚本当前使用的系统配置文件和用户配置文件的路径。你可以使用右侧浏览按钮选择新的配置文件路径。</p>
|
||||
<h4>导出配置:</h4>
|
||||
<p>选择导出配置文件的目标路径和文件名,点击‘导出配置文件’按钮,将当前的配置项导出。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>2. 使用配置文件:在脚本可执行文件的根目录创建系统配置文件system.json和用户配置文件users.json。</p>
|
||||
<div id="use-file" class="tabs-container">
|
||||
<div class="tab-buttons">
|
||||
<div class="tab-button active" data-tab="system.config">系统配置文件</div>
|
||||
<div class="tab-button" data-tab="users.config">用户配置文件</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div id="system.config" class="tab-pane active">
|
||||
<p>system.json文件控制工具的基本运行参数:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"library"</span>: {
|
||||
<span class="property">"host_url"</span>: <span class="string">"http://10.1.20.7"</span>,
|
||||
<span class="property">"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="bool">true</span>,
|
||||
<span class="property">"max_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="bool">false</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ol>
|
||||
<p><strong>library/host_url</strong>: 图书馆主机URL,无需更改。</p>
|
||||
<p><strong>library/login_url</strong>: 登录页面URL,无需更改。</p>
|
||||
<p><strong>mode/run_mode</strong>: 运行模式,可组合使用(+1:自动预约/+2:自动签到/+4:自动续约)</p>
|
||||
<p><strong>login/auto_captcha</strong>: 自动验证码识别,建议保持true</p>
|
||||
<p><strong>login/max_attempt</strong>: 登录尝试次数,默认3次</p>
|
||||
<p><strong>web_driver/driver_type</strong>: 浏览器类型(edge/chrome/firefox)</p>
|
||||
<p><strong>web_driver/driver_path</strong>: 驱动文件路径</p>
|
||||
<p><strong>web_driver/headless</strong>: 无头模式,默认false运行时显示浏览器窗口</p>
|
||||
</ol>
|
||||
</div>
|
||||
<div id="users.config" class="tab-pane">
|
||||
<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">"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">"begin_time"</span>: {
|
||||
<span class="property">"time"</span>: <span class="string">"09:30"</span>,
|
||||
<span class="property">"max_diff"</span>: <span class="number">30</span>,
|
||||
<span class="property">"prefer_early"</span>: <span class="bool">true</span>
|
||||
},
|
||||
<span class="property">"end_time"</span>: {
|
||||
<span class="property">"time"</span>: <span class="string">"21:23"</span>,
|
||||
<span class="property">"max_diff"</span>: <span class="number">30</span>,
|
||||
<span class="property">"prefer_early"</span>: <span class="bool">false</span>
|
||||
},
|
||||
<span class="property">"seat_id"</span>: <span class="string">"31A"</span>,
|
||||
<span class="property">"expect_duration"</span>: <span class="number">6</span>
|
||||
<span class="property">"satisfy_duration"</span>: <span class="bool">true</span>
|
||||
}
|
||||
},
|
||||
/* 可以添加多个上述的配置块,每个用户预约信息独立配置 */
|
||||
]
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ol>
|
||||
<p><strong>username</strong>: 学号</p>
|
||||
<p><strong>password</strong>: 密码</p>
|
||||
<p><strong>reserve_info/date</strong>: 预约日期(格式:YYYY-MM-DD)</p>
|
||||
<p><strong>reserve_info/place</strong>: 图书馆或者字符“1”</p>
|
||||
<p><strong>reserve_info/floor</strong>: 预约楼层(“2”:二层,“3”:三层,“4”:四层,“5”:五层)</p>
|
||||
<p><strong>reserve_info/room</strong>: 预约房间()</p>
|
||||
<p><strong>reserve_info/seat_id</strong>: 座位编号(例如:“12A/12a/012A/012a”)</p>
|
||||
<p><strong>reserve_info/begin_time</strong>: 预约开始时间(格式:HH:mm)</p>
|
||||
<p><strong>reserve_info/begin_time/max_diff</strong>: 最大时间差(分钟)</p>
|
||||
<p><strong>reserve_info/begin_time/prefer_early</strong>: 是否优先预约较早时间(默认true)</p>
|
||||
<p><strong>reserve_info/end_time</strong>: 预约结束时间(格式:HH:mm)</p>
|
||||
<p><strong>reserve_info/end_time/max_diff</strong>: 最大时间差(分钟)</p>
|
||||
<p><strong>reserve_info/end_time/prefer_early</strong>: 是否优先预约较早时间(默认true)</p>
|
||||
<p><strong>reserve_info/expect_duration</strong>: 期望使用时长(小时)</p>
|
||||
<p><strong>reserve_info/satisfy_duration</strong>: 是否满足期望时长(默认true)</p>
|
||||
</ol>
|
||||
<div class="info">
|
||||
<strong>提示:</strong> 可以添加多个用户,工具会按顺序处理每个用户的预约请求。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>监控运行状态</h3>
|
||||
<p>如果系统设置中没有勾选浏览器无头模式运行,工具会在运行过程中打开浏览器窗口,显示自动运行过程。</p>
|
||||
<p>除此之外,你还可以通过软件的运行日志输出区域查看详细的运行状态和错误信息。</p>
|
||||
<div class="step-image">
|
||||
<img src="./监控运行状态-运行图.png" alt="监控运行状态">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>查看运行结果</h3>
|
||||
<p>软件运行结束后日志会显示本次运行结果:“处理完成, 共计 n 个用户, 成功 n 个用户, 失败 m 个用户”。</p>
|
||||
<div class="step-image">
|
||||
<img src="./监控运行状态-运行结果.png" alt="查看运行结果">
|
||||
</div>
|
||||
</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>自动预约</h3>
|
||||
<p>如果用户当前没有有效预约时,工具会自动为您预约指定座位。</p>
|
||||
<div class="info">
|
||||
<strong>适用场景:</strong> 提前预约第二天的座位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">✅</div>
|
||||
<h3>自动签到</h3>
|
||||
<p>如果用户当前已有预约,且在可签到时间范围(开始时间的前后30分钟)内,工具会自动完成签到。</p>
|
||||
<div class="info">
|
||||
<strong>适用场景:</strong> 因忘记签到而导致失约,影响正常使用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h3>自动续约</h3>
|
||||
<p>如果用户当前正在使用座位,且到达可续约时间(结束时间前的120分钟),工具会自动延长使用时间。</p>
|
||||
<div class="info">
|
||||
<strong>适用场景:</strong> 需要长时间使用座位
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>模式组合使用</h3>
|
||||
<p>运行模式可以组合使用,只需在配置窗口中勾选对应模式即可:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<ol><strong>自动预约 + 自动签到 + 自动续约(推荐)</strong></ol>
|
||||
<ol>自动预约</ol>
|
||||
<ol>自动预约 + 自动签到</ol>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="troubleshooting" class="section">
|
||||
<h2>故障排除</h2>
|
||||
<h3>常见问题及解决方法</h3>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">工具启动时报错"无法找到驱动/Unable to obtain driver"等类似报错信息</div>
|
||||
<div class="faq-answer">
|
||||
<p>这是大概率是因为浏览器驱动未正确安装或版本不匹配。</p>
|
||||
<ul>
|
||||
<ol>1,检查驱动文件是否放置在正确位置</ol>
|
||||
<ol>2,确认驱动版本与浏览器版本完全匹配,例如:Chrome浏览器需要对应版本的chromedriver.exe,切勿混用</ol>
|
||||
<ol>3,尝试重新下载并安装驱动</ol>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">登录失败,提示账号密码错误</div>
|
||||
<div class="faq-answer">
|
||||
<p>请检查配置界面中的账号密码是否正确。</p>
|
||||
<ul>
|
||||
<ol>1,确认学号和密码无误</ol>
|
||||
<ol>2,检查是否有不支持的特殊字符需要转义</ol>
|
||||
<ol>3,尝试手动登录图书馆系统确认账号可用</ol>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">预约失败,提示座位不可用</div>
|
||||
<div class="faq-answer">
|
||||
<p>目标座位可能已被他人预约或不在可预约时间。</p>
|
||||
<ul>
|
||||
<ol>1,确认座位编号是否正确,是否在该楼层指定区域</ol>
|
||||
<ol>2,尝试预约其它座位或调整预约时间,例如调整允许的开始或结束时间的最大偏差,位置紧张情况下可以让脚本根据允许的时间范围选择最佳起始时间</ol>
|
||||
</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>根据图书馆规定,每个账号同一时间段只能预约一个座位。但您可以在配置界面中添加多个账号,工具会依次处理每个账号的预约请求。</p>
|
||||
<div class="important">
|
||||
<p><strong>重要:</strong>本工具软件旨在简化并辅助用户正常使用时的图书馆服务流程,请勿滥用影响他人及图书馆正常运行。</p>
|
||||
</div>
|
||||
</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 alpha-v0.03</a>
|
||||
<div class="info" style="margin-top: 1.5rem;">
|
||||
<p>文件大小:约98MB</p>
|
||||
<p>系统要求:Windows 10/11,支持Edge/Chrome/Firefox浏览器</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>安装步骤</h3>
|
||||
<ol>
|
||||
<ol>下载压缩包并解压到任意文件夹</ol>
|
||||
<ol>根据您使用的浏览器下载对应版本的驱动</ol>
|
||||
<ol>按照本手册说明配置账号密码等参数</ol>
|
||||
<ol>点击启动脚本,即可开始自动预约和使用座位</ol>
|
||||
</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('.tabs-container').forEach(container => {
|
||||
const tabButtons = container.querySelectorAll('.tab-button');
|
||||
const tabPanes = container.querySelectorAll('.tab-pane');
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const containerButtons = this.closest('.tabs-container').querySelectorAll('.tab-button');
|
||||
const containerPanes = this.closest('.tabs-container').querySelectorAll('.tab-pane');
|
||||
|
||||
containerButtons.forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
|
||||
containerPanes.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: '-45% 0px -45% 0px',
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
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 @@
|
||||
For more infomation, please visit our website: https://www.autolibrary.cv
|
||||
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 49 KiB |
@@ -19,7 +19,9 @@ from PySide6.QtWidgets import (
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
|
||||
from .Ui_ALConfigWidget import Ui_ALConfigWidget
|
||||
from .SeatMapWidget import SeatMapWidget
|
||||
|
||||
from .SeatMapTable import seats_maps
|
||||
from ConfigReader import ConfigReader
|
||||
from ConfigWriter import ConfigWriter
|
||||
|
||||
@@ -32,27 +34,24 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
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"))}",
|
||||
"system": "",
|
||||
"users": ""
|
||||
}
|
||||
):
|
||||
|
||||
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)
|
||||
self.__config_data = {"system": {}, "users": {}}
|
||||
self.__seat_map_widget = None
|
||||
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initlizeDefaultConfigPaths()
|
||||
if not self.initlizeConfigs():
|
||||
self.close()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
@@ -69,6 +68,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
|
||||
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
|
||||
self.SelectSeatsButton.clicked.connect(self.onSelectSeatsButtonClicked)
|
||||
self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
|
||||
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
|
||||
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
|
||||
@@ -129,42 +129,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
def initlizeDefaultConfigPaths(
|
||||
self
|
||||
) -> dict:
|
||||
):
|
||||
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
return {
|
||||
self.__default_config_paths = {
|
||||
"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,
|
||||
@@ -180,6 +154,63 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
|
||||
|
||||
|
||||
def initlizeConfig(
|
||||
self,
|
||||
which: str
|
||||
) -> bool:
|
||||
|
||||
msg = ""
|
||||
is_success = True
|
||||
if which == "system":
|
||||
system_config_path = self.__config_paths[which]
|
||||
if not os.path.exists(system_config_path):
|
||||
self.__config_data[which] = self.defaultSystemConfig()
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if self.saveSystemConfig(self.__config_paths[which], self.__config_data[which]):
|
||||
msg += f"系统配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||
else:
|
||||
is_success = False
|
||||
else:
|
||||
self.__config_data[which] = self.loadSystemConfig(system_config_path)
|
||||
if self.__config_data[which] is None:
|
||||
is_success = False
|
||||
elif which == "users":
|
||||
users_config_path = self.__config_paths[which]
|
||||
if not os.path.exists(users_config_path):
|
||||
self.__config_data[which] = self.defaultUsersConfig()
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if self.saveUsersConfig(self.__config_paths[which], self.__config_data[which]):
|
||||
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
|
||||
else:
|
||||
is_success = False
|
||||
else:
|
||||
self.__config_data[which] = self.loadUsersConfig(users_config_path)
|
||||
if self.__config_data[which] is None:
|
||||
is_success = False
|
||||
if msg:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
f"配置文件初始化完成: \n{msg}"
|
||||
)
|
||||
return is_success
|
||||
|
||||
|
||||
def initlizeConfigs(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
is_success = True
|
||||
for which in ["system", "users"]:
|
||||
if not self.__config_paths[which]:
|
||||
self.__config_paths[which] = self.__default_config_paths[which]
|
||||
if not self.initlizeConfig(which):
|
||||
is_success = False
|
||||
break
|
||||
self.initlizeConfigToWidget(which, self.__config_data[which])
|
||||
return is_success
|
||||
|
||||
|
||||
def defaultSystemConfig(
|
||||
self
|
||||
) -> dict:
|
||||
@@ -261,21 +292,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.UsernameEdit.setText("")
|
||||
self.PasswordEdit.setText("")
|
||||
self.UserListWidget.setSortingEnabled(True)
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Password)
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self.ShowPasswordCheckBox.setChecked(False)
|
||||
self.FloorComboBox.setCurrentIndex(1) # use for the '__init__' to effect the signal
|
||||
self.FloorComboBox.setCurrentIndex(0)
|
||||
self.onFloorComboBoxCurrentIndexChanged()
|
||||
self.DateEdit.setDate(QDate.currentDate())
|
||||
self.DateEdit.setMinimumDate(QDate.currentDate())
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate())
|
||||
if QTime.currentTime() > QTime(18, 0, 0) and QTime.currentTime() < QTime(23, 0, 0):
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate().addDays(1))
|
||||
self.BeginTimeEdit.setTime(QTime.currentTime())
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(False)
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(10)
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(30)
|
||||
self.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
|
||||
self.PreferLateEndTimeCheckBox.setChecked(False)
|
||||
self.MaxEndTimeDiffSpinBox.setValue(10)
|
||||
self.MaxEndTimeDiffSpinBox.setValue(30)
|
||||
self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
|
||||
self.SatisfyDurationCheckBox.setChecked(False)
|
||||
|
||||
@@ -451,21 +479,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
) -> bool:
|
||||
|
||||
if users_config_path:
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
self.__config_data["users"] = 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)
|
||||
self.__config_data["users"]["users"].append(user_config)
|
||||
if not self.saveUsersConfig(
|
||||
users_config_path,
|
||||
self.__users_config_data
|
||||
self.__config_data["users"]
|
||||
):
|
||||
return False
|
||||
if system_config_path:
|
||||
self.__system_config_data = self.collectSystemConfigFromWidget()
|
||||
self.__config_data["system"] = self.collectSystemConfigFromWidget()
|
||||
if not self.saveSystemConfig(
|
||||
system_config_path,
|
||||
self.__system_config_data
|
||||
self.__config_data["system"]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
@@ -489,12 +517,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
system_config = self.loadSystemConfig(config_path)
|
||||
users_config = self.loadUsersConfig(config_path)
|
||||
if system_config is not None:
|
||||
self.__system_config_data.update(system_config)
|
||||
self.setSystemConfigToWidget(self.__system_config_data)
|
||||
self.__config_data["system"].update(system_config)
|
||||
self.setSystemConfigToWidget(self.__config_data["system"])
|
||||
return True
|
||||
if users_config is not None:
|
||||
self.__users_config_data.update(users_config)
|
||||
self.fillUsersList(self.__users_config_data)
|
||||
self.__config_data["users"].update(users_config)
|
||||
self.fillUsersList(self.__config_data["users"])
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
@@ -528,12 +556,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
"seat_id": "",
|
||||
"begin_time": {
|
||||
"time": f"{QTime.currentTime().toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"max_diff": 30,
|
||||
"prefer_early": False
|
||||
},
|
||||
"end_time": {
|
||||
"time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"max_diff": 30,
|
||||
"prefer_early": True
|
||||
},
|
||||
"expect_duration": 2.0,
|
||||
@@ -577,6 +605,41 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
self.RoomComboBox.addItems(self.__floor_room_map[floor])
|
||||
self.RoomComboBox.setCurrentIndex(0)
|
||||
|
||||
@Slot()
|
||||
def onSeatMapWidgetClosed(
|
||||
self,
|
||||
selected_seats: list[str]
|
||||
):
|
||||
|
||||
self.__seat_map_widget.seatMapWidgetClosed.disconnect(self.onSeatMapWidgetClosed)
|
||||
self.__seat_map_widget.deleteLater()
|
||||
self.__seat_map_widget = None
|
||||
if len(selected_seats) == 0:
|
||||
return
|
||||
self.SeatIDEdit.setText(",".join(selected_seats))
|
||||
|
||||
@Slot()
|
||||
def onSelectSeatsButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
floor = self.FloorComboBox.currentText()
|
||||
room = self.RoomComboBox.currentText()
|
||||
floor_idx = self.__floor_rmap[floor]
|
||||
room_idx = self.__room_rmap[room]
|
||||
if self.__seat_map_widget is None:
|
||||
self.__seat_map_widget = SeatMapWidget(
|
||||
self,
|
||||
floor,
|
||||
room,
|
||||
seats_maps[floor_idx][room_idx]
|
||||
)
|
||||
self.__seat_map_widget.seatMapWidgetClosed.connect(self.onSeatMapWidgetClosed)
|
||||
self.__seat_map_widget.show()
|
||||
self.__seat_map_widget.raise_()
|
||||
self.__seat_map_widget.activateWindow()
|
||||
self.__seat_map_widget.selectSeats(self.SeatIDEdit.text().split(","))
|
||||
|
||||
@Slot()
|
||||
def onUserListWidgetCurrentItemChanged(
|
||||
self,
|
||||
@@ -621,7 +684,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
browser_driver_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择浏览器驱动 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
self.BrowseBrowserDriverEdit.text(),
|
||||
"可执行文件 (*.exe);;所有文件 (*)"
|
||||
)[0]
|
||||
if browser_driver_path:
|
||||
@@ -700,19 +763,22 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
users_config_path = self.ExportUserConfigEdit.text()
|
||||
if system_config_path:
|
||||
if self.saveConfigs(
|
||||
system_config_path,
|
||||
users_config_path=""
|
||||
system_config_path, ""
|
||||
):
|
||||
msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
|
||||
else:
|
||||
msg += f"系统配置文件导出失败: \n'{system_config_path}'\n"
|
||||
if users_config_path:
|
||||
if self.saveConfigs(
|
||||
"", users_config_path
|
||||
):
|
||||
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
|
||||
else:
|
||||
msg += f"用户配置文件导出失败: \n'{users_config_path}'\n"
|
||||
if msg:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
"提示 - AutoLibrary",
|
||||
msg
|
||||
)
|
||||
|
||||
@@ -748,21 +814,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
exist_files.append(users_config_path)
|
||||
reply = QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
"提示 - 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_data["system"] = self.defaultSystemConfig()
|
||||
self.__config_data["users"] = self.defaultUsersConfig()
|
||||
self.__config_paths = {
|
||||
"system": system_config_path,
|
||||
"users": users_config_path
|
||||
}
|
||||
self.initlizeConfigToWidget("system", self.__system_config_data)
|
||||
self.initlizeConfigToWidget("users", self.__users_config_data)
|
||||
self.initlizeConfigToWidget("system", self.__config_data["system"])
|
||||
self.initlizeConfigToWidget("users", self.__config_data["users"])
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
@@ -770,16 +836,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
):
|
||||
|
||||
if self.UserListWidget.currentItem() is not None:
|
||||
user = self.collectUserConfigFromUserInfoWidget()
|
||||
if user:
|
||||
self.UserListWidget.currentItem().setData(Qt.UserRole, user)
|
||||
user_config = self.collectUserConfigFromUserInfoWidget()
|
||||
if user_config:
|
||||
self.UserListWidget.currentItem().setData(Qt.UserRole, user_config)
|
||||
if self.saveConfigs(
|
||||
self.__config_paths["system"],
|
||||
self.__config_paths["users"]
|
||||
):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
"提示 - AutoLibrary",
|
||||
"配置文件保存成功 !\n"
|
||||
f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
|
||||
f"用户配置文件路径: \n{self.__config_paths['users']}"
|
||||
|
||||
@@ -232,8 +232,8 @@
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="UsernameKabel">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="PasswordLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
@@ -247,7 +247,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>用户名:</string>
|
||||
<string>密码:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -299,25 +299,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="PasswordLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>密码:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="UsernameEdit">
|
||||
<property name="minimumSize">
|
||||
@@ -340,6 +321,25 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="UsernameKabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>用户名:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -376,8 +376,65 @@
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="4" column="4">
|
||||
<widget class="QLineEdit" name="SeatIDEdit">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="DateLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>日期:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLabel" name="MaxBeginTimeDiffLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>最大时长偏差:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="PlaceLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>地点:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="4">
|
||||
<widget class="QDoubleSpinBox" name="ExpectDurationSpinBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
@@ -390,6 +447,40 @@
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>8.000000000000000</double>
|
||||
</property>
|
||||
<property name="stepType">
|
||||
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="4">
|
||||
<widget class="QCheckBox" name="PreferLateEndTimeCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>勾选此项,如果有多个与目标<span style=" font-weight:700; font-style:italic;">结束时间</span>相近的可选时间,选择时间最晚项</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>优先选择最晚</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
@@ -411,11 +502,27 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QComboBox" name="PlaceComboBox">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="BeginTimeLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>开始时间:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<widget class="QComboBox" name="FloorComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
@@ -430,13 +537,28 @@
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>图书馆</string>
|
||||
<string>二层</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>三层</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>四层</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>五层</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="QLabel" name="ExpectDurationLabel">
|
||||
<item row="10" column="1">
|
||||
<widget class="QLabel" name="MaxEndTimeDiffLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
@@ -450,7 +572,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>期望时长:</string>
|
||||
<string>最大时长偏差:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -487,8 +609,34 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="DateLabel">
|
||||
<item row="0" column="4">
|
||||
<widget class="QDateEdit" name="DateEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="calendarPopup">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="date">
|
||||
<date>
|
||||
<year>2025</year>
|
||||
<month>11</month>
|
||||
<day>1</day>
|
||||
</date>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="QLabel" name="ExpectDurationLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
@@ -502,7 +650,26 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>日期:</string>
|
||||
<string>期望时长:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLabel" name="RoomLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>区域:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -546,6 +713,99 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="4">
|
||||
<widget class="QCheckBox" name="SatisfyDurationCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>勾选此项,会优先满足预约时长限制,当座位紧张时可能会导致<span style=" font-weight:700; font-style:italic;">预约失败</span></p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>优先满足时长要求</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="4">
|
||||
<widget class="QSpinBox" name="MaxBeginTimeDiffSpinBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>期望的<span style=" font-weight:700; font-style:italic;">开始时间</span>不可用时,按照该偏差范围寻找最近可选时间</p></body></html></string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>120</number>
|
||||
</property>
|
||||
<property name="stepType">
|
||||
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="FloorLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>楼层:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QComboBox" name="PlaceComboBox">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>图书馆</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="4">
|
||||
<widget class="QCheckBox" name="PreferEarlyBeginTimeCheckBox">
|
||||
<property name="minimumSize">
|
||||
@@ -571,25 +831,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QLabel" name="MaxEndTimeDiffLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>最大时长偏差:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="4">
|
||||
<widget class="QSpinBox" name="MaxEndTimeDiffSpinBox">
|
||||
<property name="minimumSize">
|
||||
@@ -610,61 +851,9 @@
|
||||
<property name="maximum">
|
||||
<number>120</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLabel" name="RoomLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
<property name="stepType">
|
||||
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>区域:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<widget class="QComboBox" name="FloorComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>二层</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>三层</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>四层</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>五层</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="4">
|
||||
@@ -683,91 +872,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="4">
|
||||
<widget class="QSpinBox" name="MaxBeginTimeDiffSpinBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>期望的<span style=" font-weight:700; font-style:italic;">开始时间</span>不可用时,按照该偏差范围寻找最近可选时间</p></body></html></string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>120</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="4">
|
||||
<widget class="QCheckBox" name="PreferLateEndTimeCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>勾选此项,如果有多个与目标<span style=" font-weight:700; font-style:italic;">结束时间</span>相近的可选时间,选择时间最晚项</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>优先选择最晚</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="BeginTimeLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>开始时间:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="FloorLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>楼层:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="SeatIDLabel">
|
||||
<property name="minimumSize">
|
||||
@@ -787,113 +891,50 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="4">
|
||||
<widget class="QDoubleSpinBox" name="ExpectDurationSpinBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>期望的预约时长,脚本会尽量满足</p></body></html></string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>8.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QDateEdit" name="DateEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="date">
|
||||
<date>
|
||||
<year>2025</year>
|
||||
<month>11</month>
|
||||
<day>1</day>
|
||||
</date>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLabel" name="MaxBeginTimeDiffLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>最大时长偏差:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="PlaceLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>地点:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="4">
|
||||
<widget class="QCheckBox" name="SatisfyDurationCheckBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>勾选此项,会优先满足预约时长限制,当座位紧张时可能会导致<span style=" font-weight:700; font-style:italic;">预约失败</span></p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>优先满足时长要求</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<item row="4" column="4">
|
||||
<layout class="QHBoxLayout" name="SeatIDLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="SeatIDEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>130</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="SelectSeatsButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>查询座位布局</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="edit-find"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -1138,7 +1179,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>详情请参阅根目录中的 manual.html</p></body></html></string>
|
||||
<string><html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.cv/docs/manual_lists.html"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></string>
|
||||
|
||||
@@ -13,7 +13,7 @@ import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
|
||||
Qt, Signal, Slot, QTimer, QThread
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QMenu
|
||||
@@ -80,6 +80,7 @@ class AutoLibWorker(QThread):
|
||||
self
|
||||
):
|
||||
|
||||
auto_lib = None
|
||||
try:
|
||||
if not self.checkTimeAvailable():
|
||||
self.showTraceSignal.emit(
|
||||
@@ -98,13 +99,14 @@ class AutoLibWorker(QThread):
|
||||
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:
|
||||
if auto_lib:
|
||||
auto_lib.close()
|
||||
self.showTraceSignal.emit("AutoLibrary 运行结束")
|
||||
self.finishedSignal.emit()
|
||||
|
||||
|
||||
@@ -128,10 +130,8 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
self.__input_queue = queue.Queue()
|
||||
self.__output_queue = queue.Queue()
|
||||
self.__config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
"users":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
|
||||
"system": "",
|
||||
"users": "",
|
||||
}
|
||||
self.__alConfigWidget = None
|
||||
self.__auto_lib_thread = None
|
||||
@@ -242,7 +242,10 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
self.__alConfigWidget = None
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.deleteLater()
|
||||
self.__alConfigWidget = None
|
||||
self.ConfigButton.setEnabled(True)
|
||||
self.StartButton.setEnabled(True)
|
||||
self.StopButton.setEnabled(False)
|
||||
@@ -272,14 +275,15 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
):
|
||||
|
||||
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)
|
||||
if self.__auto_lib_thread is None:
|
||||
self.__auto_lib_thread = AutoLibWorker(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self.__config_paths,
|
||||
)
|
||||
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
|
||||
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
|
||||
self.__auto_lib_thread.start()
|
||||
|
||||
@Slot()
|
||||
@@ -287,9 +291,16 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
self
|
||||
):
|
||||
|
||||
if self.__auto_lib_thread and self.__auto_lib_thread.isRunning():
|
||||
self.__auto_lib_thread.stop()
|
||||
if self.__auto_lib_thread:
|
||||
self.showTrace("正在停止操作......")
|
||||
self.__auto_lib_thread.stop()
|
||||
self.__auto_lib_thread.wait()
|
||||
self.showTrace("操作已停止")
|
||||
self.__auto_lib_thread.showMsgSignal.disconnect(self.showMsg)
|
||||
self.__auto_lib_thread.showTraceSignal.disconnect(self.showTrace)
|
||||
self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.deleteLater()
|
||||
self.__auto_lib_thread = None
|
||||
self.setControlButtons(True, True, False)
|
||||
|
||||
@Slot()
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QLabel
|
||||
)
|
||||
|
||||
|
||||
class SeatFrame(QFrame):
|
||||
|
||||
clicked = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
seat_number,
|
||||
parent=None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__seat_number = seat_number
|
||||
self.__is_selected = False
|
||||
self.setUpUi()
|
||||
|
||||
def setUpUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setFixedSize(60, 40)
|
||||
self.setFrameStyle(QFrame.Box | QFrame.Plain)
|
||||
self.setLineWidth(2)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.label = QLabel(self.__seat_number, self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.label.setGeometry(0, 0, 60, 40)
|
||||
|
||||
def mousePressEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.toggleSelection()
|
||||
self.clicked.emit(self.__seat_number)
|
||||
|
||||
|
||||
def isSelected(
|
||||
self
|
||||
):
|
||||
|
||||
return self.__is_selected
|
||||
|
||||
|
||||
def toggleSelection(self):
|
||||
|
||||
self.__is_selected = not self.__is_selected
|
||||
if self.__is_selected:
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #388E3C;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
@@ -0,0 +1,270 @@
|
||||
seats_maps = {
|
||||
"2": {
|
||||
"1": """
|
||||
,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,,
|
||||
,,,,,,,,,,,039C,039D,,040C,040D,,041C,041D,,042C,042D,,043C,043D,,044C,044D,,,,,,,,,
|
||||
038B,038D,,037B,037D,,036B,036D,,,,,,,,,,,,,,,,,,,,,,045C,045A,,046C,046A,,047C,047A
|
||||
038A,038C,,037A,037C,,036A,036C,,,,,,,,,,,,,,,,,,,,,,045D,045B,,046D,046B,,047D,047B
|
||||
035B,035D,,034B,034D,,033B,033D,,,,,,,,,,,,,,,,,,,,,,048C,048A,,049C,049A,,050C,050A
|
||||
035A,035C,,034A,034C,,033A,033C,,,,,,,,,,,,,,,,,,,,,,048D,048B,,049D,049B,,050D,050B
|
||||
032B,032D,,031B,031D,,030B,030D,,,,,,,,,,,,,,,,,,,,,,051C,051A,,052C,052A,,053C,053A
|
||||
032A,032C,,031A,031C,,030A,030C,,,,,,,,,,,,,,,,,,,,,,051D,051B,,052D,052B,,053D,053B
|
||||
029B,029D,,028B,028D,,027B,027D,,,,,,,,,,,,,,,,,,,,,,054C,054A,,055C,055A,,056C,056A
|
||||
029A,029C,,028A,028C,,027A,027C,,,,,,,,,,,,,,,,,,,,,,054D,054B,,055D,055B,,056D,056B
|
||||
026B,026D,,025B,025D,,024B,024D,,,,,,,,,,,,,,,,,,,,,,057C,057A,,058C,058A,,059C,059A
|
||||
026A,026C,,025A,025C,,024A,024C,,,,,,,,,,,,,,,,,,,,,,057D,057B,,058D,058B,,059D,059B
|
||||
023B,023D,,022B,022D,,021B,021D,,,,,,,,,,,,,,,,,,,,,,060C,060A,,061C,061A,,062C,062A
|
||||
023A,023C,,022A,022C,,021A,021C,,,,,,,,,,,,,,,,,,,,,,060D,060B,,061D,061B,,062D,062B
|
||||
020B,020D,,019B,019D,,018B,018D,,,,,,,,,,,,,,,,,,,,,,063C,063A,,064C,064A,,065C,065A
|
||||
020A,020C,,019A,019C,,018A,018C,,,,,,,,,,,,,,,,,,,,,,063D,063B,,064D,064B,,065D,065B
|
||||
,,,,,,,,,,,017D,017C,,014D,014C,,011D,011C,,008D,008C,,005D,005C,,002D,002C,001D,001C,,,,,,,
|
||||
,,,,,,,,,,,017B,017A,,014B,014A,,011B,011A,,008B,008A,,005B,005A,,002B,002A,001B,001A,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,073D,073C,,015D,015C,,012D,012C,,,,,006D,006C,,003D,003C,,,,,,,,,
|
||||
,,,,,,,,,,,073B,073A,,015B,015A,,012B,012A,,,,,006B,006A,,003B,003A,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,072D,072C,,016D,016C,,013D,013C,,,,,007D,007C,,004D,004C,,,,,,,,,
|
||||
,,,,,,,,,,,072B,072A,,016B,016A,,013B,013A,,,,,007B,007A,,004B,004A,,,,,,,,,
|
||||
,,,,,,,,,,,071D,071C,,070D,070C,,069D,069C,,068D,068C,,067D,067C,,066D,066C,,,,,,,,,
|
||||
,,,,,,,,,,,071B,071A,,070B,070A,,069B,069A,,068B,068A,,067B,067A,,066B,066A,,,,,,,,,
|
||||
""",
|
||||
"2": """
|
||||
023B,023D,024B,024D,,,,,,,,,,,,,,,
|
||||
023A,023C,024A,024C,,,,,,,,,,,,,,,
|
||||
022B,022D,032D,032C,,,,,,,,,,,,,,,
|
||||
022A,022C,032B,032A,,,,,,,,,,,,,,,
|
||||
021B,021D,,,,,,,,,,,,,,,,,
|
||||
021A,021C,,,,,,,,,,,,,,,,,
|
||||
020B,020D,,,,,,,,,,,,,,,,,
|
||||
020A,020C,,,,,,,,,,,,,,,,,
|
||||
019B,019D,,,,,,,,,,,,,,,,,
|
||||
019A,019C,,,,,,,,,,,,,,,,,
|
||||
018B,018D,,,,,,,,,,,,,,,,,
|
||||
018A,018C,,,,,,,,,,,,,,,,,
|
||||
017B,017D,,,,,,,,,,,,,,,,,
|
||||
017A,017C,,,,,,,,,,,,,,,,,
|
||||
016B,016D,,,,,,,,,,,,,,,,,
|
||||
016A,016C,,,,,031A,031C,,,,,,,,,,,
|
||||
015B,015D,,,,,030B,030D,,,,,,,,,,,
|
||||
015A,015C,,,,,030A,030C,,,,,,,,,,,
|
||||
014B,014D,,,,,029B,029D,,,,,,,,,,,
|
||||
014A,014C,,,,,029A,029C,,,,,,,,,,,
|
||||
013B,013D,,,,,028B,028D,,,,,,,,,,,
|
||||
013A,013C,,,,,028A,028C,,,,,,,,,,,
|
||||
012B,012D,,,,,027B,027D,,,,,,,,,,,
|
||||
012A,012C,,,,,027A,027C,,,,,,,,,,,
|
||||
011B,011D,,,,,026B,026D,,,,,,,,,,,
|
||||
011A,011C,,,,,026A,026C,,,,,,,,,,,
|
||||
010B,010D,,,,,025B,025D,,,,,,,,,,,
|
||||
010A,010C,,,,,,,,,,,,,,,,,
|
||||
009B,009D,,,,,,,,,,,,,,,,,
|
||||
009A,009C,,,,,,,,,,,,,,,,,
|
||||
008B,008D,,,,,,,,,,,,,,,,,
|
||||
008A,008C,,,,,,,,,,,,,,,,,
|
||||
007B,007D,,,,,,,,,,,,,,,,,
|
||||
007A,007C,,,,,,,,,,,,,,,,,
|
||||
006B,006D,,,,,,,,,,,,,,,,,
|
||||
006A,006C,,,,,,,,,,,,,,,,,
|
||||
005B,005D,,,,,,,,,,,,,,,,,
|
||||
005A,005C,,,,,,,,,,,,,,,,,
|
||||
004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,
|
||||
004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,
|
||||
|
||||
"""
|
||||
},
|
||||
"3": {
|
||||
"3": """
|
||||
,,007B,007D,,,,,,,,008C,008A,,
|
||||
,,007A,007C,,,,,,,,008D,008B,,
|
||||
,,006B,006D,,,,,,,,009C,009A,,
|
||||
,,006A,006C,,,,,,,,009D,009B,,
|
||||
,,005B,005D,,,,,,,,010C,010a,,
|
||||
,,005A,005C,,,,,,,,010D,010B,,
|
||||
,,004B,004D,,,,,,,,011C,011A,,
|
||||
,,004A,004C,,,,,,,,011D,011B,,
|
||||
,,003B,003D,,,,,,,,012C,012A,,
|
||||
,,003A,003C,,,,,,,,012D,012B,,
|
||||
,,002B,002D,,,,,,,,013C,013A,,
|
||||
,,002A,002C,,,,,,,,013D,013B,,
|
||||
,,001B,001D,,,,,,,,014C,014A,,
|
||||
,,001A,001C,,,,,,,,014D,014B,,
|
||||
""",
|
||||
"4": """
|
||||
,,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,056D,056C,057D,057C,,
|
||||
,,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,056B,056A,057B,057A,,
|
||||
036B,036D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058C,058A,,060C,060A
|
||||
036A,036C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058D,058B,,060D,060B
|
||||
035B,035D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059C,059A,,061C,061A
|
||||
035A,035C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059D,059B,,061D,061B
|
||||
034B,034D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062C,062A
|
||||
034A,034C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062D,062B
|
||||
033B,033D,,,,,,,,,,,,080B,080D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,063C,063A
|
||||
033A,033C,,,,,,,,,,,,080A,080C,,081A,081B,082A,082B,083A,083B,084A,084B,085A,085B,086A,086B,087A,,,,,,,,,,,,,,,,,,063D,063B
|
||||
032B,032D,,,,,,,,,,,,079B,079D,,081C,081D,082C,082D,083C,083D,084C,084D,085C,085D,086C,086D,087C,,,,,,,,,,,,,,,,,,064C,064A
|
||||
032A,032C,,,,,,,,,,,,079A,079C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,064D,064B
|
||||
031B,031D,,,,,,,,,,,,078B,078D,,,,,,,,,,,,,,088A,088C,,,,,,,,,,,,,,,,,065C,065A
|
||||
031A,031C,,,,,,,,,,,,078A,078C,,,,,,,,,,,,,,088B,088D,,,,,,,,,,,,,,,,,065D,065B
|
||||
030B,030D,,,,,,,,,,,,077B,077D,,,,,,,,,,,,,,089A,089C,,,,,,,,,,,,,,,,,066C,066A
|
||||
030A,030C,,,,,,,,,,,,077A,077C,,,,,,,,,,,,,,089B,089D,,,,,,,,,,,,,,,,,066D,066B
|
||||
029B,029D,,,,,,,,,,,,076B,076D,,,,,,,,,,,,,,090A,090C,,,,,,,,,,,,,,,,,,
|
||||
029A,029C,,,,,,,,,,,,076A,076C,,,,,,,,,,,,,,090B,090D,,,,,,,,,,,,,,,,,,
|
||||
028B,028D,,,,,,,,,,,,075B,075D,,,,,,,,,,,,,,091A,091C,,,,,,,,,,,,,,,,,,
|
||||
028A,028C,,,,,,,,,,,,075A,075C,,,,,,,,,,,,,,091B,091D,,,,,,,,,,,,,,,,,,
|
||||
027B,027D,,,,,,,,,,,,074B,074D,,,,,,,,,,,,,,092A,092C,,,,,,,,,,,,,,,,,,
|
||||
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,092B,092D,,,,,,,,,,,,,,,,,,
|
||||
026B,026D,,,,,,,,,,,,,,,073D,073C,072D,072C,071D,071C,070D,070C,069D,069C,068D,068C,,,,,,,,,,,,,,,,,,,,
|
||||
026A,026C,,,,,,,,,,,,,,,073B,073A,072B,072A,071B,071A,070B,070A,069B,069A,068B,068A,,,,,,,,,,,,,,,,,,,,
|
||||
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
024B,024D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
024A,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
023B,023D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
023A,023C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067C,,
|
||||
022B,022D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067B,,
|
||||
022A,022C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067A,,
|
||||
,,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,
|
||||
,,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007a,006B,006A,005B,005A,004B,004A,003b,003A,002B,002A,001B,001A,,,,
|
||||
|
||||
"""
|
||||
},
|
||||
"4": {
|
||||
"5": """
|
||||
,,,,,,,,042A,042B,045A,045B,048A,048B,051A,051B,054A,054B,057A,057B,060A,060B,,,,,,
|
||||
,,,,,,,,042C,042D,045C,045D,048C,048D,051C,051D,054C,054D,057C,057D,060C,060D,,,,,,
|
||||
,,,,,,,,041A,041B,044A,044B,047A,047B,050A,050B,053A,053B,056A,056B,059A,059B,,,,,,
|
||||
,,,,,,,,041C,041D,044C,044D,047C,047D,050C,050D,053C,053D,056C,056D,059C,059D,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,040A,040B,043A,043B,046A,046B,049A,049B,052A,052B,055A,055B,058A,058B,,,,,,
|
||||
,,,,,,,,040C,040D,043C,043D,046C,046D,049C,049D,052C,052D,055C,055D,058C,058D,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,039B,039D,038B,038D,,037B,037D,,,,,,,,,,,,,,,,,,,,,
|
||||
,039A,039C,038A,038C,,037A,037C,,,,,,,,,,,,,,,,,,,,,
|
||||
,036B,036D,035B,035D,,034B,034D,,,,,,,,,,,,,,,,,,,,,
|
||||
,036A,036C,035A,035C,,034A,034C,,,,,,,,,,,,,,,,,,,,,
|
||||
,033B,033D,032B,032D,,031B,031D,,,,,,,,,,,,,,,,,,,,,
|
||||
,033A,033C,032A,032C,,031A,031C,,,,,,,,,,,,,,,,,,,,,
|
||||
,030B,030D,029B,029D,,028B,028D,,,,,,,,,,,,,,,,,,,,,
|
||||
,030A,030C,029A,029C,,028A,028C,,,,,,,,,,,,,,,,,,,,,
|
||||
,027B,027D,026B,026D,,025B,025D,,,,,,,,,,,,,,,,,,,,,
|
||||
,027A,027C,026A,026C,,025A,025C,,,,,,,,,,,,,,,,,,,,,
|
||||
,024B,024D,023B,023D,,022B,022D,,,,,,,,,,,,,,,,,,,,,
|
||||
,024A,024C,023A,023C,,022A,022C,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,019D,019C,016D,016C,013D,013C,010D,010C,007D,007C,004D,004C,001D,001C,,,,,,
|
||||
,,,,,,,,019B,019A,016B,016A,013B,013A,010B,010A,007B,007A,004B,004A,001B,001A,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,020D,020C,017D,017C,014D,014C,011D,011C,008D,008C,005D,005C,002D,002C,,,,,,
|
||||
,,,,,,,,020B,020A,017B,017A,014B,014A,011B,011A,008B,008A,005B,005A,002B,002A,,,,,,
|
||||
,,,,,,,,021D,021C,018D,018C,015D,015C,012D,012C,009D,009C,006D,006C,003D,003C,,,,,,
|
||||
,,,,,,,,021B,021A,018B,018A,015B,015A,012B,012A,009B,009A,006B,006A,003B,003A,,,,,,
|
||||
|
||||
""",
|
||||
"6": """
|
||||
,,,026C,026D,027D,027C,028D,028C,029D,029C,030D,030C,031D,031C,032D,032C,033D,033C,035D,035C,036D,036C,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,046D,046C
|
||||
,,,026A,026B,027B,027A,028B,028A,029B,029A,030B,030A,031B,031A,032B,032A,033B,033A,035B,035A,036B,036A,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,046B,046A
|
||||
025D,025C,,,,,,,,,,,,,,,,034D,034C,,,,,,,,,,,,,,,,,,,,,,,047C,047A
|
||||
025B,025A,,,,,,,,,,,,,,,,034B,034A,,,,,,,,,,,,,,,,,,,,,,,047D,047B
|
||||
024D,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,048C,048A
|
||||
024B,024A,,,,,,,,,,,,,,050D,050C,052D,052C,054D,054C,056D,056C,058D,058C,060D,060C,,,,,,,,,,,,,,,048D,048B
|
||||
023D,023C,,,,,,,,,,,,,,050B,050A,052B,052A,054B,054A,056B,056A,058B,058A,060B,060A,,,,,,,,,,,,,,,,
|
||||
023B,023A,,,,,,,,,,,,,,049D,049C,051D,051C,053D,053C,055D,055C,057D,057C,059D,059C,,,,,,,,,,,,,,,,
|
||||
022D,022C,,,,,,,,,,,,,,049B,049A,051B,051A,053B,053A,055B,055A,057B,057A,059B,059A,,,,,,,,,,,,,,,,
|
||||
022B,022A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
021D,021C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
021B,021A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
020D,020C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
020B,020A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
019D,019C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
019B,019A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
015D,015C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
015B,015A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
014D,014C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
014B,014A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
013D,013C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
013B,013A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
012D,012C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
012B,012A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
011D,011C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
011B,011A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
010D,010C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
010B,010A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
009D,009C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
009B,009A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
008D,008C,,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
008B,008A,,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
||||
""",
|
||||
"7": """
|
||||
,,,,,,,,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,,,,,,,,,,,,
|
||||
,,,,,,,,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
|
||||
016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
|
||||
|
||||
"""
|
||||
},
|
||||
"5": {
|
||||
"8": """
|
||||
,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,056C,056A,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
045B,045D,,,,,,,,,,,,,,,,,,,,,,056D,056B,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
045A,045C,,,,,,,,,,,,,,,,,,,,,,057C,057A,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
044B,044D,,,,,,,,,,,,,,,,,,,,,,057D,057B,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
044A,044C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
043B,043D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
043A,043C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
042B,042D,,,,,,,,,,,,,,,,,070B,070D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
042A,042C,,,,,,,,,,,,,,,,,070A,070C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
041B,041D,,,,,,,,,,,,,,,,,069B,069D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
041A,041C,,,,,,,,,,,,,,,,,069A,069C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
040B,040D,,,,,,,,,,,,,,,,,068B,068D,,071A,071B,072A,072B,073A,073B,074A,074B,075A,075B,076A,076B,077A,077B,,,,,,,,,,,,,,,,
|
||||
040A,040C,,,,,,,,,,,,,,,,,068A,068C,,071C,071D,072C,072D,073C,073D,074C,074D,075C,075D,076C,076D,077C,077D,,,,,,,,,,,,,,,,
|
||||
039B,039D,,,,,,,,,,,,,,,,,067B,067D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
039A,039C,,,,,,,,,,,,,,,,,067A,067C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
038B,038D,,,,,,,,,,,,,,,,,066B,066D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
038A,038C,,,,,,,,,,,,,,,,,066A,066C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
037B,037D,,,,,,,,,,,,,,,,,065B,065D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
037A,037C,,,,,,,,,,,,,,,,,065A,065C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
036B,036D,,,,,,,,,,,,,,,,,064B,064D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
036A,036C,,,,,,,,,,,,,,,,,064A,064C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
035B,035D,,,,,,,,,,,,,,,,,063B,063D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
035A,035C,,,,,,,,,,,,,,,,,063A,063C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
034B,034D,,,,,,,,,,,,,,,,,062B,062D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
034A,034C,,,,,,,,,,,,,,,,,062A,062C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
033B,033D,,,,,,,,,,,,,,,,,,,061D,061C,,060D,060C,,059D,059C,,058D,058C,,,,,,,,,,,,,,,,,,,,
|
||||
033A,033C,,,,,,,,,,,,,,,,,,,061B,061A,,060B,060A,,059B,059A,,058B,058A,,,,,,,,,,,,,,,,,,,,
|
||||
032B,032D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
032A,032C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
031B,031D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
031A,031C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
030B,030D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
030A,030C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
029B,029D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
029A,029C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
028B,028D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
028A,028C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
027B,027D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
026B,026D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
026A,026C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,024D,024C,023D,023C,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
|
||||
,,,024B,024A,023B,023A,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from PySide6.QtCore import (
|
||||
Qt, Slot, Signal, QEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout,
|
||||
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem,
|
||||
QPushButton,
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QPainter, QWheelEvent, QCloseEvent
|
||||
)
|
||||
from .SeatFrame import SeatFrame
|
||||
|
||||
|
||||
class SeatMapWidget(QWidget):
|
||||
|
||||
seatMapWidgetClosed = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget = None,
|
||||
floor: str = "",
|
||||
room: str = "",
|
||||
seats_data: dict = {},
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.__floor = floor
|
||||
self.__room = room
|
||||
self.__seats_data = seats_data
|
||||
self.__selected_seats = []
|
||||
self.__seat_frames = {}
|
||||
self.setUpUi()
|
||||
self.connectSignals()
|
||||
|
||||
@staticmethod
|
||||
def formatSeatNumber(
|
||||
seat_number: str
|
||||
) -> str:
|
||||
|
||||
if seat_number and not seat_number[-1].isdigit():
|
||||
digits = seat_number[:-1]
|
||||
letter = seat_number[-1]
|
||||
return digits.zfill(3) + letter
|
||||
return seat_number.zfill(3)
|
||||
|
||||
|
||||
def setUpUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowFlags(Qt.WindowType.Window)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(800, 600)
|
||||
self.resize(800, 600)
|
||||
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
|
||||
|
||||
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
|
||||
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
|
||||
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
|
||||
|
||||
self.SeatMapGraphicsView = QGraphicsView(self)
|
||||
self.SeatMapGraphicsScene = QGraphicsScene(self)
|
||||
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
|
||||
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
|
||||
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
||||
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.viewport().installEventFilter(self)
|
||||
|
||||
self.SeatsContainerWidget = QWidget()
|
||||
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
|
||||
self.createSeatMap()
|
||||
|
||||
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
|
||||
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
|
||||
|
||||
self.TipsLabel = QLabel(
|
||||
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
|
||||
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
|
||||
)
|
||||
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
|
||||
|
||||
self.ConfirmButton = QPushButton("确认")
|
||||
self.ConfirmButton.setFixedSize(80, 25)
|
||||
self.CancelButton = QPushButton("取消")
|
||||
self.CancelButton.setFixedSize(80, 25)
|
||||
self.SeatMapWidgetControlLayout = QHBoxLayout()
|
||||
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
|
||||
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.seatMapWidgetClosed.emit(self.__selected_seats)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def eventFilter(
|
||||
self,
|
||||
watched,
|
||||
event
|
||||
):
|
||||
|
||||
if (watched is self.SeatMapGraphicsView.viewport() and
|
||||
event.type() == QEvent.Type.Wheel and
|
||||
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||||
):
|
||||
self.zoomGraphicsView(event)
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
|
||||
def zoomGraphicsView(
|
||||
self,
|
||||
event: QWheelEvent
|
||||
):
|
||||
|
||||
delta = event.angleDelta().y()
|
||||
zoom_factor = 1.2 if delta > 0 else 1/1.2
|
||||
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor)
|
||||
|
||||
|
||||
def createSeatMap(
|
||||
self
|
||||
):
|
||||
|
||||
rows = self.__seats_data.strip().split("\n")
|
||||
for row_idx, row in enumerate(rows):
|
||||
col_idx = 0
|
||||
seats_number = [seat.strip() for seat in row.split(",")]
|
||||
for seat_number in seats_number:
|
||||
if seat_number:
|
||||
seat_widget = SeatFrame(seat_number)
|
||||
seat_widget.clicked.connect(self.onSeatClicked)
|
||||
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
|
||||
self.__seat_frames[seat_number] = seat_widget
|
||||
else:
|
||||
spacer = QFrame()
|
||||
spacer.setFixedSize(20, 30)
|
||||
spacer.setStyleSheet("background-color: transparent; border: none;")
|
||||
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
|
||||
col_idx += 1
|
||||
self.SeatsContainerLayout.setSpacing(20)
|
||||
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
|
||||
self.SeatsContainerWidget.adjustSize()
|
||||
|
||||
|
||||
def selectSeat(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if len(self.__selected_seats) >= 1:
|
||||
return
|
||||
seat_number = self.formatSeatNumber(seat_number)
|
||||
if seat_number not in self.__seat_frames:
|
||||
return
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
return
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats.append(seat_number)
|
||||
|
||||
|
||||
def selectSeats(
|
||||
self,
|
||||
selected_seats: list
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
for seat_number in selected_seats:
|
||||
self.selectSeat(seat_number)
|
||||
|
||||
|
||||
def getSelectedSeats(
|
||||
self
|
||||
) -> list[str]:
|
||||
|
||||
return self.__selected_seats
|
||||
|
||||
|
||||
def clearSelections(
|
||||
self
|
||||
):
|
||||
|
||||
seats_to_clear = self.__selected_seats.copy()
|
||||
for seat_number in seats_to_clear:
|
||||
if seat_number not in self.__seat_frames:
|
||||
continue
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats = []
|
||||
|
||||
@Slot(str)
|
||||
def onSeatClicked(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if seat_number in self.__selected_seats:
|
||||
self.__selected_seats.remove(seat_number)
|
||||
else:
|
||||
if len(self.__selected_seats) < 1:
|
||||
self.__selected_seats.append(seat_number)
|
||||
else:
|
||||
self.__seat_frames[seat_number].toggleSelection()
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
self.close()
|
||||