Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55ae4d0d96 | |||
| 7dcd72939b | |||
| bfce61f4b4 | |||
| 60a5699822 | |||
| aab9565012 | |||
| 9255eec9f1 | |||
| cff6fd8fc0 | |||
| b129f47b48 | |||
| 069429be71 | |||
| 7d064fc8e7 | |||
| 1b172ad396 | |||
| 05c9d433f4 | |||
| 65ca40438d | |||
| 0a8763add5 | |||
| c5e589f3d1 | |||
| 5e5deba773 | |||
| 842fb434f4 | |||
| 6cabddf0cd | |||
| 0322558339 | |||
| 703ee527ae | |||
| 9a925fecb6 | |||
| 189fddfb6a | |||
| c2d53a8b78 | |||
| b99431476a | |||
| 977c0835b7 | |||
| cd565ec57d | |||
| 9f17474c1b | |||
| 04d66346dc | |||
| f858295af1 | |||
| cd6c899388 | |||
| 1038a86aff | |||
| 15ea47dd07 | |||
| 829a8440ad | |||
| 389ac885d3 | |||
| 68b61b5c8c | |||
| fd5abb5f1e | |||
| 1f16181aeb | |||
| f0c25903a3 | |||
| b24e4f473f | |||
| 8bb65be0b9 | |||
| 631785122b | |||
| 82ea40d3dc | |||
| 1244084c75 |
@@ -8,10 +8,14 @@ build/
|
||||
dist/
|
||||
model/*.onnx
|
||||
driver/*.exe
|
||||
gui/configs/*.json
|
||||
gui/translators/qtbase_zh_CN.qm
|
||||
gui/AutoLibraryResources.py
|
||||
gui/AutoLibraryResource.py
|
||||
gui/Ui_ALMainWindow.py
|
||||
gui/Ui_ALConfigWidget.py
|
||||
src/gui/configs/*.json
|
||||
src/gui/translators/qtbase_zh_CN.qm
|
||||
src/gui/AutoLibraryResources.py
|
||||
src/gui/AutoLibraryResource.py
|
||||
src/gui/Ui_ALMainWindow.py
|
||||
src/gui/Ui_ALConfigWidget.py
|
||||
src/gui/Ui_ALTimerTaskWidget.py
|
||||
src/gui/Ui_ALAddTimerTaskDialog.py
|
||||
src/gui/Ui_ALAboutDialog.py
|
||||
|
||||
Main.spec
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import queue
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibChecker(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def __navigateToReserveRecordPage(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __getReserveRecord(
|
||||
self,
|
||||
wanted_date: str,
|
||||
wanted_status: str
|
||||
) -> dict:
|
||||
|
||||
if wanted_date is None:
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态")
|
||||
return None
|
||||
self._showTrace(f"正在检查用户在日期 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
|
||||
date_obj = datetime.strptime(wanted_date, "%Y-%m-%d").date()
|
||||
|
||||
checked_count = 0
|
||||
max_check_times = 3 # we only check (3*4=)12 reservations
|
||||
|
||||
if not self.__navigateToReserveRecordPage():
|
||||
return None
|
||||
for _ in range(max_check_times):
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList dl"
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return None
|
||||
for i in range(checked_count, len(reservations) - 1): # the last one is load button
|
||||
reservation = reservations[i]
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
info_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"解析第 {i + 1} 条预约记录时发生未知错误 !")
|
||||
continue
|
||||
is_wanted = any(wanted_status in status.text for status in info_elements)
|
||||
# process time element to get the date string
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r'(\d{4}-\d{1,2}-\d{1,2})', time_str)
|
||||
if date_match:
|
||||
date_str = date_match.group(1)
|
||||
else:
|
||||
self._showTrace(f"无法解析第 {i + 1} 条预约记录的日期 ! 该记录的时间为 {time_str}")
|
||||
continue
|
||||
# reservation is later than the given date, check the next one
|
||||
if datetime.strptime(date_str, "%Y-%m-%d").date() > date_obj:
|
||||
continue
|
||||
# reservation is earlier than the given date, can reserve
|
||||
if datetime.strptime(date_str, "%Y-%m-%d").date() < date_obj:
|
||||
return None
|
||||
# query the wanted status
|
||||
if is_wanted:
|
||||
self._showTrace(f"寻找到第 {i + 1} 条预约记录, 状态为 {wanted_status}")
|
||||
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
|
||||
if time_match is None:
|
||||
self._showTrace(f"无法解析第 {i + 1} 条预约记录的时间 ! 该记录的时间为 {time_str}")
|
||||
continue
|
||||
return {
|
||||
"index": i,
|
||||
"date": date_str,
|
||||
"time_str": time_match.group(0),
|
||||
"status": wanted_status
|
||||
}
|
||||
checked_count = len(reservations) - 1
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
else:
|
||||
self._showTrace("该用户无法加载更多预约记录")
|
||||
break
|
||||
except:
|
||||
self._showTrace("加载更多预约记录失败 !")
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# no reserved or using record in the given date
|
||||
# then can reserve
|
||||
if self.__getReserveRecord(date, "已预约") is None:
|
||||
if self.__getReserveRecord(date, "使用中") is None:
|
||||
self._showTrace(f"用户在日期 {date} 可以预约")
|
||||
return True
|
||||
self._showTrace(f"用户在日期 {date} 有使用中的预约, 无法预约")
|
||||
self._showTrace(f"用户在日期 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
|
||||
|
||||
def canCheckin(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# have a reserved record in the given date
|
||||
record = self.__getReserveRecord(date, "已预约")
|
||||
if record is not None:
|
||||
time_match = re.search(r"(\d{1,2}:\d{2})", record["time_str"])
|
||||
if time_match:
|
||||
begin_time = time_match.group(0)
|
||||
begin_time = datetime.strptime(f"{date} {begin_time}", "%Y-%m-%d %H:%M")
|
||||
time_diff = datetime.now() - begin_time
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# before 30 minutes, cant checkin
|
||||
if time_diff_seconds < -30*60:
|
||||
self._showTrace(
|
||||
f"用户在日期 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"距离当前时间还有 {abs(time_diff_seconds)/60:.2f} 分钟, 无法签到"
|
||||
)
|
||||
return False
|
||||
# before in 30 minutes, can checkin
|
||||
elif -30*60 <= time_diff_seconds < 0:
|
||||
self._showTrace(
|
||||
f"用户在日期 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"距离当前时间还有 {abs(time_diff_seconds)/60:.2f} 分钟, 可以签到"
|
||||
)
|
||||
return True
|
||||
# past less than 30 minutes, can checkin
|
||||
elif 0 <= time_diff_seconds < 30*60:
|
||||
self._showTrace(
|
||||
f"用户在日期 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前时间已经 {abs(time_diff_seconds)/60:.2f} 分钟, 可以签到"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"用户在日期 {date} 的预约时间格式错误, 无法签到")
|
||||
self._showTrace(f"用户在日期 {date} 有没有有效预约记录, 无法签到")
|
||||
return False
|
||||
@@ -1,40 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import queue
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibCheckin(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -1,34 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import queue
|
||||
|
||||
from LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibRenew(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -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 |
@@ -1,800 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QLineEdit, QMessageBox, QFileDialog, QListWidgetItem
|
||||
)
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
|
||||
from .Ui_ALConfigWidget import Ui_ALConfigWidget
|
||||
|
||||
from ConfigReader import ConfigReader
|
||||
from ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
configWidgetCloseSingal = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
"users":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
|
||||
}
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.modifyUi()
|
||||
self.__config_paths = config_paths
|
||||
self.__system_config_data = self.loadSystemConfig(self.__config_paths["system"])
|
||||
self.__users_config_data = self.loadUsersConfig(self.__config_paths["users"])
|
||||
if not self.__system_config_data:
|
||||
self.initlizeDefaultConfig("system")
|
||||
if not self.__users_config_data:
|
||||
self.initlizeDefaultConfig("users")
|
||||
self.initlizeConfigToWidget("system", self.__system_config_data)
|
||||
self.initlizeConfigToWidget("users", self.__users_config_data)
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initilizeUserInfoWidget()
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
|
||||
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
|
||||
self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
|
||||
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
|
||||
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
|
||||
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
|
||||
self.BrowseCurrentSystemConfigButton.clicked.connect(self.onBrowseCurrentSystemConfigButtonClicked)
|
||||
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
|
||||
self.BrowseExportSystemConfigButton.clicked.connect(self.onBrowseExportSystemConfigButtonClicked)
|
||||
self.BrowseExportUserConfigButton.clicked.connect(self.onBrowseExportUserConfigButtonClicked)
|
||||
self.ExportConfigButton.clicked.connect(self.onExportConfigButtonClicked)
|
||||
self.NewConfigButton.clicked.connect(self.onNewConfigButtonClicked)
|
||||
self.LoadConfigButton.clicked.connect(self.onLoadConfigButtonClicked)
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.configWidgetCloseSingal.emit(self.__config_paths)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def initlizeFloorRoomMap(
|
||||
self
|
||||
):
|
||||
|
||||
self.__floor_map = {
|
||||
"2": "二层",
|
||||
"3": "三层",
|
||||
"4": "四层",
|
||||
"5": "五层"
|
||||
}
|
||||
self.__room_map = {
|
||||
"1": "二层内环",
|
||||
"2": "二层外环",
|
||||
"3": "三层内环",
|
||||
"4": "三层外环",
|
||||
"5": "四层内环",
|
||||
"6": "四层外环",
|
||||
"7": "四层期刊区",
|
||||
"8": "五层考研"
|
||||
}
|
||||
self.__floor_rmap = {
|
||||
v: k for k, v in self.__floor_map.items()
|
||||
}
|
||||
self.__room_rmap = {
|
||||
v: k for k, v in self.__room_map.items()
|
||||
}
|
||||
self.__floor_room_map = {
|
||||
"二层": ["二层内环", "二层外环"],
|
||||
"三层": ["三层内环", "三层外环"],
|
||||
"四层": ["四层内环", "四层外环", "四层期刊区"],
|
||||
"五层": ["五层考研"]
|
||||
}
|
||||
|
||||
|
||||
def initlizeDefaultConfigPaths(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
return {
|
||||
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
|
||||
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json"))
|
||||
}
|
||||
|
||||
|
||||
def initlizeDefaultConfig(
|
||||
self,
|
||||
which: str
|
||||
):
|
||||
|
||||
default_config_paths = self.initlizeDefaultConfigPaths()
|
||||
if which == "system":
|
||||
self.__system_config_data = self.defaultSystemConfig()
|
||||
self.__config_paths["system"] = default_config_paths["system"]
|
||||
self.saveSystemConfig(self.__config_paths["system"], self.__system_config_data)
|
||||
elif which == "users":
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
self.__config_paths["users"] = default_config_paths["users"]
|
||||
self.saveUsersConfig(self.__config_paths["users"], self.__users_config_data)
|
||||
if which == "system":
|
||||
file_type = "系统配置文件"
|
||||
elif which == "users":
|
||||
file_type = "用户配置文件"
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
f"{file_type}已初始化, \n"\
|
||||
f" 文件路径: {self.__config_paths[which]}"
|
||||
)
|
||||
|
||||
|
||||
def initlizeConfigToWidget(
|
||||
self,
|
||||
which: str,
|
||||
config_data: dict
|
||||
):
|
||||
|
||||
if which == "system":
|
||||
self.setSystemConfigToWidget(config_data)
|
||||
self.CurrentSystemConfigEdit.setText(self.__config_paths["system"])
|
||||
elif which == "users":
|
||||
self.initilizeUserInfoWidget()
|
||||
self.fillUsersList(config_data)
|
||||
self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
|
||||
|
||||
|
||||
def defaultSystemConfig(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"library": {
|
||||
"host_url": "http://10.1.20.7",
|
||||
"login_url": "/login"
|
||||
},
|
||||
"login": {
|
||||
"auto_captcha": True,
|
||||
"max_attempt": 3
|
||||
},
|
||||
"web_driver": {
|
||||
"driver_type": "edge",
|
||||
"driver_path": "msedgedriver.exe",
|
||||
"headless": False
|
||||
},
|
||||
"mode": {
|
||||
"run_mode": 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def defaultUsersConfig(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"users": []
|
||||
}
|
||||
|
||||
|
||||
def collectSystemConfigFromWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
system_config = self.defaultSystemConfig()
|
||||
# library config is never changed
|
||||
system_config["login"]["auto_captcha"] = self.AutoCaptchaCheckBox.isChecked()
|
||||
system_config["login"]["max_attempt"] = self.LoginAttemptSpinBox.value()
|
||||
system_config["web_driver"]["driver_type"] = self.BrowserTypeComboBox.currentText()
|
||||
system_config["web_driver"]["driver_path"] = self.BrowseBrowserDriverEdit.text()
|
||||
system_config["web_driver"]["headless"] = self.HeadlessCheckBox.isChecked()
|
||||
run_mode = 0
|
||||
if self.AutoReserveCheckBox.isChecked():
|
||||
run_mode |= 0x01
|
||||
if self.AutoCheckinCheckBox.isChecked():
|
||||
run_mode |= 0x02
|
||||
if self.AutoRenewalCheckBox.isChecked():
|
||||
run_mode |= 0x04
|
||||
system_config["mode"]["run_mode"] = run_mode
|
||||
return system_config
|
||||
|
||||
|
||||
def setSystemConfigToWidget(
|
||||
self,
|
||||
system_config: dict
|
||||
):
|
||||
|
||||
self.HostUrlEdit.setText(system_config["library"]["host_url"])
|
||||
self.LoginUrlEdit.setText(system_config["library"]["login_url"])
|
||||
self.AutoCaptchaCheckBox.setChecked(system_config["login"]["auto_captcha"])
|
||||
self.LoginAttemptSpinBox.setValue(system_config["login"]["max_attempt"])
|
||||
self.BrowserTypeComboBox.setCurrentText(system_config["web_driver"]["driver_type"])
|
||||
driver_path = os.path.abspath(system_config["web_driver"]["driver_path"])
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
|
||||
self.HeadlessCheckBox.setChecked(system_config["web_driver"]["headless"])
|
||||
run_mode = system_config["mode"]["run_mode"]
|
||||
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
|
||||
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
|
||||
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
|
||||
|
||||
|
||||
def initilizeUserInfoWidget(
|
||||
self
|
||||
):
|
||||
|
||||
self.UsernameEdit.setText("")
|
||||
self.PasswordEdit.setText("")
|
||||
self.UserListWidget.setSortingEnabled(True)
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Password)
|
||||
self.ShowPasswordCheckBox.setChecked(False)
|
||||
self.FloorComboBox.setCurrentIndex(1) # use for the '__init__' to effect the signal
|
||||
self.FloorComboBox.setCurrentIndex(0)
|
||||
self.DateEdit.setDate(QDate.currentDate())
|
||||
self.DateEdit.setMinimumDate(QDate.currentDate())
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate())
|
||||
if QTime.currentTime() > QTime(18, 0, 0) and QTime.currentTime() < QTime(23, 0, 0):
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate().addDays(1))
|
||||
self.BeginTimeEdit.setTime(QTime.currentTime())
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(False)
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(10)
|
||||
self.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
|
||||
self.PreferLateEndTimeCheckBox.setChecked(False)
|
||||
self.MaxEndTimeDiffSpinBox.setValue(10)
|
||||
self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
|
||||
self.SatisfyDurationCheckBox.setChecked(False)
|
||||
|
||||
|
||||
def collectUserConfigFromUserInfoWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
user_config = {
|
||||
"username": self.UsernameEdit.text(),
|
||||
"password": self.PasswordEdit.text(),
|
||||
"reserve_info": {
|
||||
"begin_time":{},
|
||||
"end_time": {}
|
||||
}
|
||||
}
|
||||
user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd")
|
||||
user_config["reserve_info"]["place"] = self.PlaceComboBox.currentText()
|
||||
user_config["reserve_info"]["floor"] = self.__floor_rmap[self.FloorComboBox.currentText()]
|
||||
user_config["reserve_info"]["room"] = self.__room_rmap[self.RoomComboBox.currentText()]
|
||||
user_config["reserve_info"]["seat_id"] = self.SeatIDEdit.text()
|
||||
user_config["reserve_info"]["begin_time"]["time"] = self.BeginTimeEdit.time().toString("HH:mm")
|
||||
user_config["reserve_info"]["begin_time"]["max_diff"] = self.MaxBeginTimeDiffSpinBox.value()
|
||||
user_config["reserve_info"]["begin_time"]["prefer_early"] = self.PreferEarlyBeginTimeCheckBox.isChecked()
|
||||
user_config["reserve_info"]["end_time"]["time"] = self.EndTimeEdit.time().toString("HH:mm")
|
||||
user_config["reserve_info"]["end_time"]["max_diff"] = self.MaxEndTimeDiffSpinBox.value()
|
||||
user_config["reserve_info"]["end_time"]["prefer_early"] = not self.PreferLateEndTimeCheckBox.isChecked()
|
||||
user_config["reserve_info"]["expect_duration"] = self.ExpectDurationSpinBox.value()
|
||||
user_config["reserve_info"]["satisfy_duration"] = self.SatisfyDurationCheckBox.isChecked()
|
||||
return user_config
|
||||
|
||||
|
||||
def collectUserConfigFromUserListWidget(
|
||||
self,
|
||||
index: int
|
||||
) -> dict:
|
||||
|
||||
user_config = self.defaultUsersConfig()
|
||||
if index < 0 or index >= self.UserListWidget.count():
|
||||
return user_config
|
||||
user_item = self.UserListWidget.item(index)
|
||||
if user_item:
|
||||
user_config = user_item.data(Qt.UserRole)
|
||||
return user_config
|
||||
|
||||
|
||||
def setUserConfigToWidget(
|
||||
self,
|
||||
user_config: dict
|
||||
) -> None:
|
||||
|
||||
try:
|
||||
self.UsernameEdit.setText(user_config["username"])
|
||||
self.PasswordEdit.setText(user_config["password"])
|
||||
self.DateEdit.setDate(QDate.fromString(user_config["reserve_info"]["date"], "yyyy-MM-dd"))
|
||||
self.PlaceComboBox.setCurrentText(user_config["reserve_info"]["place"])
|
||||
self.FloorComboBox.setCurrentText(self.__floor_map[user_config["reserve_info"]["floor"]])
|
||||
self.RoomComboBox.setCurrentText(self.__room_map[user_config["reserve_info"]["room"]])
|
||||
self.SeatIDEdit.setText(user_config["reserve_info"]["seat_id"])
|
||||
self.BeginTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["begin_time"]["time"], "H:mm"))
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(user_config["reserve_info"]["begin_time"]["max_diff"])
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(user_config["reserve_info"]["begin_time"]["prefer_early"])
|
||||
self.EndTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["end_time"]["time"], "H:mm"))
|
||||
self.MaxEndTimeDiffSpinBox.setValue(user_config["reserve_info"]["end_time"]["max_diff"])
|
||||
self.PreferLateEndTimeCheckBox.setChecked(not user_config["reserve_info"]["end_time"]["prefer_early"])
|
||||
self.ExpectDurationSpinBox.setValue(user_config["reserve_info"]["expect_duration"])
|
||||
self.SatisfyDurationCheckBox.setChecked(user_config["reserve_info"]["satisfy_duration"])
|
||||
except:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"用户配置文件读取发生错误 !\n"\
|
||||
f"用户: {user_config['username']} 配置文件可能已损坏"
|
||||
)
|
||||
|
||||
|
||||
def loadSystemConfig(
|
||||
self,
|
||||
system_config_path: str
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
if not system_config_path or not os.path.exists(system_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
system_config = ConfigReader(system_config_path).getConfigs()
|
||||
if system_config and "library" in system_config\
|
||||
and "web_driver" in system_config\
|
||||
and "login" in system_config:
|
||||
return system_config
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"系统配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {system_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveSystemConfig(
|
||||
self,
|
||||
system_config_path: str,
|
||||
system_config_data: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not system_config_path:
|
||||
raise Exception("文件路径为空")
|
||||
if not system_config_data or not isinstance(system_config_data, dict):
|
||||
raise Exception("系统配置数据为空或类型错误")
|
||||
ConfigWriter(system_config_path, system_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: {system_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def loadUsersConfig(
|
||||
self,
|
||||
users_config_path: str
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
if not users_config_path or not os.path.exists(users_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
users_config = ConfigReader(users_config_path).getConfigs()
|
||||
if users_config and "users" in users_config:
|
||||
return users_config
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {users_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveUsersConfig(
|
||||
self,
|
||||
users_config_path: str,
|
||||
users_config_data: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not users_config_path:
|
||||
raise Exception("文件路径为空")
|
||||
if not users_config_data or not isinstance(users_config_data, dict):
|
||||
raise Exception("用户配置数据为空或类型错误")
|
||||
ConfigWriter(users_config_path, users_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: \n{users_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def saveConfigs(
|
||||
self,
|
||||
system_config_path: str,
|
||||
users_config_path: str
|
||||
) -> bool:
|
||||
|
||||
if users_config_path:
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
for index in range(self.UserListWidget.count()):
|
||||
user_config = self.collectUserConfigFromUserListWidget(index)
|
||||
if user_config:
|
||||
self.__users_config_data["users"].append(user_config)
|
||||
if not self.saveUsersConfig(
|
||||
users_config_path,
|
||||
self.__users_config_data
|
||||
):
|
||||
return False
|
||||
if system_config_path:
|
||||
self.__system_config_data = self.collectSystemConfigFromWidget()
|
||||
if not self.saveSystemConfig(
|
||||
system_config_path,
|
||||
self.__system_config_data
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def loadConfig(
|
||||
self,
|
||||
config_path: str
|
||||
) -> bool:
|
||||
|
||||
if not config_path:
|
||||
config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"从现有配置文件中加载 - AutoLibrary",
|
||||
f"{QDir.toNativeSeparators(QDir.currentPath())}",
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if not config_path:
|
||||
return False
|
||||
try:
|
||||
system_config = self.loadSystemConfig(config_path)
|
||||
users_config = self.loadUsersConfig(config_path)
|
||||
if system_config is not None:
|
||||
self.__system_config_data.update(system_config)
|
||||
self.setSystemConfigToWidget(self.__system_config_data)
|
||||
return True
|
||||
if users_config is not None:
|
||||
self.__users_config_data.update(users_config)
|
||||
self.fillUsersList(self.__users_config_data)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def fillUsersList(
|
||||
self,
|
||||
users_config_data: list[dict]
|
||||
):
|
||||
|
||||
self.UserListWidget.clear()
|
||||
if "users" in users_config_data:
|
||||
for user in users_config_data["users"]:
|
||||
user_item = QListWidgetItem(user["username"])
|
||||
user_item.setData(Qt.UserRole, user)
|
||||
self.UserListWidget.addItem(user_item)
|
||||
|
||||
|
||||
def addUser(
|
||||
self
|
||||
):
|
||||
|
||||
new_user = {
|
||||
"username": f"新用户-{self.UserListWidget.count()}",
|
||||
"password": "000000",
|
||||
"reserve_info": {
|
||||
"date": f"{QDate.currentDate().toString("yyyy-MM-dd")}",
|
||||
"place": "\u56fe\u4e66\u9986",
|
||||
"floor": "2",
|
||||
"room": "1",
|
||||
"seat_id": "",
|
||||
"begin_time": {
|
||||
"time": f"{QTime.currentTime().toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"prefer_early": False
|
||||
},
|
||||
"end_time": {
|
||||
"time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"prefer_early": True
|
||||
},
|
||||
"expect_duration": 2.0,
|
||||
"satisfy_duration": False
|
||||
}
|
||||
}
|
||||
user_item = QListWidgetItem(new_user["username"])
|
||||
user_item.setData(Qt.UserRole, new_user)
|
||||
self.UserListWidget.addItem(user_item)
|
||||
self.UserListWidget.setCurrentItem(user_item)
|
||||
self.setUserConfigToWidget(new_user)
|
||||
|
||||
|
||||
def delUser(
|
||||
self
|
||||
):
|
||||
|
||||
current_item = self.UserListWidget.currentItem()
|
||||
if current_item:
|
||||
self.UserListWidget.takeItem(self.UserListWidget.row(current_item))
|
||||
self.UserListWidget.setCurrentItem(None)
|
||||
|
||||
@Slot()
|
||||
def onShowPasswordCheckBoxChecked(
|
||||
self,
|
||||
checked: bool
|
||||
):
|
||||
|
||||
if checked:
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Normal)
|
||||
else:
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Password)
|
||||
|
||||
@Slot()
|
||||
def onFloorComboBoxCurrentIndexChanged(
|
||||
self,
|
||||
):
|
||||
|
||||
floor = self.FloorComboBox.currentText()
|
||||
self.RoomComboBox.clear()
|
||||
self.RoomComboBox.addItems(self.__floor_room_map[floor])
|
||||
self.RoomComboBox.setCurrentIndex(0)
|
||||
|
||||
@Slot()
|
||||
def onUserListWidgetCurrentItemChanged(
|
||||
self,
|
||||
current: QListWidgetItem,
|
||||
previous: QListWidgetItem
|
||||
):
|
||||
# dont care about the 'self.__users_config_data', we already
|
||||
# cant effectively update the data of each user, due to the
|
||||
# possiblity of frequency edit. we just let the QListWidget
|
||||
# help us.
|
||||
if not current:
|
||||
self.initilizeUserInfoWidget()
|
||||
return
|
||||
if previous:
|
||||
user = self.collectUserConfigFromUserInfoWidget()
|
||||
if user:
|
||||
previous.setText(user["username"])
|
||||
previous.setData(Qt.UserRole, user)
|
||||
user = current.data(Qt.UserRole)
|
||||
if user:
|
||||
self.setUserConfigToWidget(user)
|
||||
|
||||
@Slot()
|
||||
def onAddUserButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.addUser()
|
||||
|
||||
@Slot()
|
||||
def onDelUserButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.delUser()
|
||||
|
||||
@Slot()
|
||||
def onBrowseBrowserDriverButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
browser_driver_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择浏览器驱动 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"可执行文件 (*.exe);;所有文件 (*)"
|
||||
)[0]
|
||||
if browser_driver_path:
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
|
||||
|
||||
@Slot()
|
||||
def onBrowseCurrentSystemConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
system_config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择其它的系统配置 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if system_config_path:
|
||||
system_config_path = QDir.toNativeSeparators(system_config_path)
|
||||
if self.loadConfig(system_config_path):
|
||||
self.__config_paths["system"] = system_config_path
|
||||
self.CurrentSystemConfigEdit.setText(system_config_path)
|
||||
|
||||
@Slot()
|
||||
def onBrowseCurrentUserConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
users_config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择其它的用户配置 - AutoLibrary",
|
||||
self.CurrentUserConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if users_config_path:
|
||||
users_config_path = QDir.toNativeSeparators(users_config_path)
|
||||
if self.loadConfig(users_config_path):
|
||||
self.__config_paths["users"] = users_config_path
|
||||
self.CurrentUserConfigEdit.setText(users_config_path)
|
||||
|
||||
@Slot()
|
||||
def onBrowseExportSystemConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
system_config_path = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"导出系统配置 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if system_config_path:
|
||||
self.ExportSystemConfigEdit.setText(QDir.toNativeSeparators(system_config_path))
|
||||
|
||||
@Slot()
|
||||
def onBrowseExportUserConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
users_config_path = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"导出用户配置 - AutoLibrary",
|
||||
self.CurrentUserConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if users_config_path:
|
||||
self.ExportUserConfigEdit.setText(QDir.toNativeSeparators(users_config_path))
|
||||
|
||||
@Slot()
|
||||
def onExportConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
msg = ""
|
||||
|
||||
system_config_path = self.ExportSystemConfigEdit.text()
|
||||
users_config_path = self.ExportUserConfigEdit.text()
|
||||
if system_config_path:
|
||||
if self.saveConfigs(
|
||||
system_config_path,
|
||||
users_config_path=""
|
||||
):
|
||||
msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
|
||||
if users_config_path:
|
||||
if self.saveConfigs(
|
||||
"", users_config_path
|
||||
):
|
||||
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
|
||||
if msg:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
msg
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onLoadConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.loadConfig("")
|
||||
|
||||
@Slot()
|
||||
def onNewConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
file_path = self.CurrentSystemConfigEdit.text()
|
||||
folder_dir = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"选择新建配置的文件夹 - AutoLibrary",
|
||||
QDir.toNativeSeparators(QFileInfo(os.path.abspath(file_path)).absoluteDir().path())
|
||||
)
|
||||
if not folder_dir:
|
||||
return
|
||||
system_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "system.json"))
|
||||
users_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "users.json"))
|
||||
system_exists = os.path.isfile(system_config_path)
|
||||
users_exists = os.path.isfile(users_config_path)
|
||||
if system_exists or users_exists:
|
||||
exist_files = []
|
||||
if system_exists:
|
||||
exist_files.append(system_config_path)
|
||||
if users_exists:
|
||||
exist_files.append(users_config_path)
|
||||
reply = QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
f"文件夹中已存在以下文件, 是否覆盖 ?\n{chr(10).join(exist_files)}",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.No:
|
||||
return
|
||||
self.__system_config_data = self.defaultSystemConfig()
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
self.__config_paths = {
|
||||
"system": system_config_path,
|
||||
"users": users_config_path
|
||||
}
|
||||
self.initlizeConfigToWidget("system", self.__system_config_data)
|
||||
self.initlizeConfigToWidget("users", self.__users_config_data)
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.UserListWidget.currentItem() is not None:
|
||||
user = self.collectUserConfigFromUserInfoWidget()
|
||||
if user:
|
||||
self.UserListWidget.currentItem().setData(Qt.UserRole, user)
|
||||
if self.saveConfigs(
|
||||
self.__config_paths["system"],
|
||||
self.__config_paths["users"]
|
||||
):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
"配置文件保存成功 !\n"
|
||||
f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
|
||||
f"用户配置文件路径: \n{self.__config_paths['users']}"
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"配置文件保存失败, 请检查文件路径权限"
|
||||
)
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.close()
|
||||
@@ -1,304 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QMenu
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QTextCursor, QCloseEvent, QFont, QIcon
|
||||
)
|
||||
|
||||
from .Ui_ALMainWindow import Ui_ALMainWindow
|
||||
from .ALConfigWidget import ALConfigWidget
|
||||
|
||||
from . import AutoLibraryResource
|
||||
|
||||
from AutoLib import AutoLib
|
||||
from ConfigReader import ConfigReader
|
||||
|
||||
|
||||
class AutoLibWorker(QThread):
|
||||
|
||||
finishedSignal = Signal()
|
||||
showTraceSignal = Signal(str)
|
||||
showMsgSignal = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.__input_queue = input_queue
|
||||
self.__output_queue = output_queue
|
||||
self.__config_paths = config_paths
|
||||
self.__stopped = False
|
||||
|
||||
|
||||
def checkTimeAvailable(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
current_time = time.strftime("%H:%M", time.localtime())
|
||||
if current_time >= "23:30" or current_time <= "07:30":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def checkConfigPaths(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
if not all(
|
||||
os.path.exists(path) for path in self.__config_paths.values()
|
||||
):
|
||||
self.showTraceSignal.emit(
|
||||
"配置文件路径不存在, 请检查配置文件路径是否正确。"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
if not self.checkTimeAvailable():
|
||||
self.showTraceSignal.emit(
|
||||
"当前时间不在图书馆开放时间内。\n"\
|
||||
" 请在 07:30 - 23:30 之间尝试"
|
||||
)
|
||||
return
|
||||
if not self.checkConfigPaths():
|
||||
return
|
||||
self.showTraceSignal.emit("AutoLibrary 开始运行")
|
||||
auto_lib = AutoLib(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
)
|
||||
auto_lib.run(
|
||||
ConfigReader(self.__config_paths["system"]),
|
||||
ConfigReader(self.__config_paths["users"]),
|
||||
)
|
||||
auto_lib.close()
|
||||
self.showTraceSignal.emit("AutoLibrary 运行结束")
|
||||
except Exception as e:
|
||||
self.showTraceSignal.emit(
|
||||
f"AutoLibrary 运行时发生异常 : {e}"
|
||||
)
|
||||
finally:
|
||||
self.finishedSignal.emit()
|
||||
|
||||
|
||||
def stop(
|
||||
self
|
||||
):
|
||||
|
||||
self.__stopped = True
|
||||
|
||||
|
||||
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.__class_name = self.__class__.__name__
|
||||
|
||||
self.setupUi(self)
|
||||
self.__input_queue = queue.Queue()
|
||||
self.__output_queue = queue.Queue()
|
||||
self.__config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
"users":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
|
||||
}
|
||||
self.__alConfigWidget = None
|
||||
self.__auto_lib_thread = None
|
||||
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.startMsgPolling()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
|
||||
self.setWindowIcon(icon)
|
||||
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
|
||||
self.StartButton.clicked.connect(self.onStartButtonClicked)
|
||||
self.StopButton.clicked.connect(self.onStopButtonClicked)
|
||||
self.SendButton.clicked.connect(self.onSendButtonClicked)
|
||||
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent,
|
||||
):
|
||||
|
||||
if self.__timer and self.__timer.isActive():
|
||||
self.__timer.stop()
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.close()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def appendToTextEdit(
|
||||
self,
|
||||
text: str,
|
||||
):
|
||||
|
||||
cursor = self.MessageIOTextEdit.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.insertText(text + "\n")
|
||||
self.MessageIOTextEdit.setTextCursor(cursor)
|
||||
self.MessageIOTextEdit.ensureCursorVisible()
|
||||
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
|
||||
def startMsgPolling(
|
||||
self
|
||||
):
|
||||
|
||||
self.__timer = QTimer()
|
||||
self.__timer.timeout.connect(self.pollMsgQueue)
|
||||
self.__timer.start(100)
|
||||
|
||||
|
||||
def setControlButtons(
|
||||
self,
|
||||
config_button_enabled: bool,
|
||||
start_button_enabled: bool,
|
||||
stop_button_enabled: bool,
|
||||
):
|
||||
|
||||
self.ConfigButton.setEnabled(config_button_enabled)
|
||||
self.StartButton.setEnabled(start_button_enabled)
|
||||
self.StopButton.setEnabled(stop_button_enabled)
|
||||
|
||||
@Slot()
|
||||
def showMsg(
|
||||
self,
|
||||
msg: str,
|
||||
):
|
||||
|
||||
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
|
||||
|
||||
@Slot()
|
||||
def showTrace(
|
||||
self,
|
||||
msg: str,
|
||||
):
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
|
||||
|
||||
@Slot()
|
||||
def pollMsgQueue(
|
||||
self,
|
||||
):
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = self.__output_queue.get_nowait()
|
||||
self.appendToTextEdit(msg)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
@Slot(dict)
|
||||
def onConfigWidgetClosed(
|
||||
self,
|
||||
config_paths: dict,
|
||||
):
|
||||
|
||||
self.__alConfigWidget = None
|
||||
self.ConfigButton.setEnabled(True)
|
||||
self.StartButton.setEnabled(True)
|
||||
self.StopButton.setEnabled(False)
|
||||
self.__config_paths = config_paths
|
||||
|
||||
@Slot()
|
||||
def onConfigButtonClicked(
|
||||
self,
|
||||
):
|
||||
|
||||
if self.__alConfigWidget is None:
|
||||
self.__alConfigWidget = ALConfigWidget(
|
||||
self,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.setWindowFlags(Qt.Window)
|
||||
self.__alConfigWidget.setWindowModality(Qt.ApplicationModal)
|
||||
self.__alConfigWidget.show()
|
||||
self.__alConfigWidget.raise_()
|
||||
self.__alConfigWidget.activateWindow()
|
||||
self.ConfigButton.setEnabled(False)
|
||||
|
||||
@Slot()
|
||||
def onStartButtonClicked(
|
||||
self,
|
||||
):
|
||||
|
||||
self.setControlButtons(False, False, True)
|
||||
self.__auto_lib_thread = AutoLibWorker(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self.__config_paths,
|
||||
)
|
||||
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
|
||||
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
|
||||
self.__auto_lib_thread.start()
|
||||
|
||||
@Slot()
|
||||
def onStopButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__auto_lib_thread and self.__auto_lib_thread.isRunning():
|
||||
self.__auto_lib_thread.stop()
|
||||
self.showTrace("正在停止操作......")
|
||||
self.setControlButtons(True, True, False)
|
||||
|
||||
@Slot()
|
||||
def onSendButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
msg = self.MessageEdit.text().strip()
|
||||
if not msg:
|
||||
return
|
||||
self.showMsg(msg)
|
||||
self.MessageEdit.clear()
|
||||
|
Before Width: | Height: | Size: 785 KiB |
@@ -1 +1,5 @@
|
||||
## Please see in the [manual.html](./document/manual.html)
|
||||
|
||||
# AutoLibrary
|
||||
|
||||
请访问[AutoLibrary 网站](http://autolibrary.cv)\
|
||||
Please access the [AutoLibrary Website](http://autolibrary.cv)
|
||||
|
||||
@@ -9,7 +9,7 @@ See the LICENSE file for details.
|
||||
"""
|
||||
import queue
|
||||
|
||||
from MsgBase import MsgBase
|
||||
from base.MsgBase import MsgBase
|
||||
|
||||
|
||||
class LibOperator(MsgBase):
|
||||
@@ -17,14 +17,14 @@ class LibOperator(MsgBase):
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -16,7 +16,7 @@ class MsgBase:
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
self._class_name = self.__class__.__name__
|
||||
@@ -43,7 +43,7 @@ class MsgBase:
|
||||
|
||||
def _waitMsg(
|
||||
self,
|
||||
timeout: float = 1.0,
|
||||
timeout: float = 1.0
|
||||
) -> str:
|
||||
|
||||
try:
|
||||
@@ -55,7 +55,7 @@ class MsgBase:
|
||||
|
||||
def _inputMsg(
|
||||
self,
|
||||
timeout: float = 1.0,
|
||||
timeout: float = 1.0
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
"""
|
||||
Base module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- MsgBase: Base class for messages.\
|
||||
- LibOperator: Base class for library operators.
|
||||
"""
|
||||
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import sys
|
||||
import platform
|
||||
|
||||
from PySide6.QtGui import (
|
||||
QIcon
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QApplication
|
||||
)
|
||||
from PySide6.QtCore import (
|
||||
QTimer, Qt
|
||||
)
|
||||
|
||||
from gui.AppInfo import AL_VERSION
|
||||
from gui.Ui_ALAboutDialog import Ui_ALAboutDialog
|
||||
|
||||
from gui import AutoLibraryResource
|
||||
|
||||
|
||||
class ALAboutDialog(QDialog, Ui_ALAboutDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary.ico").pixmap(48, 48))
|
||||
info_text = self.generateAboutText()
|
||||
self.AboutInfoEdit.setHtml(info_text)
|
||||
self.AboutInfoEdit.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.CopyButton.clicked.connect(self.copyAboutInfo)
|
||||
|
||||
|
||||
def generateAboutText(
|
||||
self
|
||||
):
|
||||
|
||||
os_info = self.getOSInfo()
|
||||
about_text = f"""
|
||||
<h4>Version Information:</h4>
|
||||
Version: {AL_VERSION}<br>
|
||||
Python version: {platform.python_version()}<br>
|
||||
Qt version: {self.getQtVersion()}<br>
|
||||
|
||||
<h4>System Information:</h4>
|
||||
Processor: {platform.processor()}<br>
|
||||
Operating system: {os_info['system']}<br>
|
||||
System version: {os_info['version']}<br>
|
||||
System architecture: {os_info['architecture']}<br>
|
||||
|
||||
<h4>Project Information:</h4>
|
||||
License: MIT License<br>
|
||||
Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
|
||||
Project website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br>
|
||||
|
||||
<h4>Author Information:</h4>
|
||||
Developer: KenanZhu<br>
|
||||
Contact: nanoki_zh@163.com<br>
|
||||
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><br>
|
||||
"""
|
||||
return about_text
|
||||
|
||||
|
||||
def getOSInfo(
|
||||
self
|
||||
):
|
||||
|
||||
system = platform.system()
|
||||
version = platform.version()
|
||||
architecture = platform.architecture()[0]
|
||||
|
||||
if system == "Windows":
|
||||
try:
|
||||
version = platform.win32_ver()[1]
|
||||
except:
|
||||
pass
|
||||
elif system == "Darwin":
|
||||
try:
|
||||
version = platform.mac_ver()[0]
|
||||
except:
|
||||
pass
|
||||
elif system == "Linux":
|
||||
try:
|
||||
import distro # try to get Linux distro info
|
||||
version = f"{distro.name()} {distro.version()}"
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return {
|
||||
'system': system,
|
||||
'version': version,
|
||||
'architecture': architecture
|
||||
}
|
||||
|
||||
|
||||
def getQtVersion(
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
from PySide6.QtCore import qVersion
|
||||
return qVersion()
|
||||
except:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def copyAboutInfo(
|
||||
self
|
||||
):
|
||||
|
||||
about_text = self.AboutInfoEdit.toPlainText()
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(about_text)
|
||||
original_text = self.CopyButton.text()
|
||||
self.CopyButton.setText("已复制")
|
||||
QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text))
|
||||
@@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALAboutDialog</class>
|
||||
<widget class="QDialog" name="ALAboutDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>400</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>800</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>关于 - AutoLibrary</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALAboutDialogLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="LogoLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="LogoIconLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>56</width>
|
||||
<height>56</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>56</width>
|
||||
<height>56</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="LogoTextLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>24</pointsize>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>AutoLibrary</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="AboutInfoLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>56</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>56</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="AboutInfoEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier New</family>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="lineWrapMode">
|
||||
<enum>QTextEdit::LineWrapMode::NoWrap</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextInteractionFlag::TextBrowserInteraction</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="CopyButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>复制</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import queue
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDateTime
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QLabel, QDialog, QWidget, QSpinBox, QVBoxLayout,
|
||||
QHBoxLayout, QGridLayout, QDateTimeEdit
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QCloseEvent
|
||||
)
|
||||
|
||||
from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog
|
||||
|
||||
|
||||
class TimerTaskStatus(Enum):
|
||||
|
||||
PENDING = "等待中"
|
||||
READY = "已就绪"
|
||||
RUNNING = "执行中"
|
||||
EXECUTED = "已执行"
|
||||
OUTDATED = "已过期"
|
||||
|
||||
|
||||
class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.modifyUi()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTypeComboBox.setCurrentIndex(0)
|
||||
self.SpecificTimerWidget = QWidget()
|
||||
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
|
||||
self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
|
||||
self.SpecificDateTimeEdit = QDateTimeEdit()
|
||||
self.SpecificDateTimeEdit.setCalendarPopup(True)
|
||||
self.SpecificDateTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
||||
self.SpecificDateTimeEdit.setMinimumDateTime(QDateTime.currentDateTime())
|
||||
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
|
||||
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
|
||||
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
|
||||
|
||||
self.RelativeTimerWidget = QWidget()
|
||||
self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget)
|
||||
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0)
|
||||
self.RelativeDaySpinBox = QSpinBox()
|
||||
self.RelativeDaySpinBox.setMinimum(0)
|
||||
self.RelativeDaySpinBox.setMaximum(365)
|
||||
self.RelativeDaySpinBox.setSuffix("天")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0)
|
||||
self.RelativeHourSpinBox = QSpinBox()
|
||||
self.RelativeHourSpinBox.setMinimum(0)
|
||||
self.RelativeHourSpinBox.setMaximum(23)
|
||||
self.RelativeHourSpinBox.setSuffix("时")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1)
|
||||
self.RelativeMinuteSpinBox = QSpinBox()
|
||||
self.RelativeMinuteSpinBox.setMinimum(0)
|
||||
self.RelativeMinuteSpinBox.setMaximum(59)
|
||||
self.RelativeMinuteSpinBox.setSuffix("分")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2)
|
||||
self.RelativeSecondSpinBox = QSpinBox()
|
||||
self.RelativeSecondSpinBox.setMinimum(0)
|
||||
self.RelativeSecondSpinBox.setMaximum(59)
|
||||
self.RelativeSecondSpinBox.setSuffix("秒")
|
||||
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3)
|
||||
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
|
||||
self.RelativeTimerWidget.setVisible(False)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.CancelButton.clicked.connect(self.reject)
|
||||
self.ConfirmButton.clicked.connect(self.accept)
|
||||
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
|
||||
|
||||
|
||||
def getTimerTask(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
added_time = datetime.now()
|
||||
if not self.TaskNameLineEdit.text():
|
||||
name = f"未命名任务-{added_time.strftime("%Y%m%d%H%M%S")}"
|
||||
else:
|
||||
name = self.TaskNameLineEdit.text()
|
||||
timer_type_index = self.TimerTypeComboBox.currentIndex()
|
||||
silent = not self.ShowBeforeRunRadioButton.isChecked()
|
||||
if timer_type_index == 0:
|
||||
execute_time = self.SpecificDateTimeEdit.dateTime()
|
||||
tmp_time_str = execute_time.toString("yyyy-MM-dd HH:mm:ss")
|
||||
execute_time = datetime.strptime(tmp_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
execute_time = datetime.now() + timedelta(
|
||||
days = self.RelativeDaySpinBox.value(),
|
||||
hours = self.RelativeHourSpinBox.value(),
|
||||
minutes = self.RelativeMinuteSpinBox.value(),
|
||||
seconds = self.RelativeSecondSpinBox.value()
|
||||
)
|
||||
return {
|
||||
"name": name,
|
||||
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
|
||||
"time_type": self.TimerTypeComboBox.currentText(),
|
||||
"execute_time": execute_time,
|
||||
"silent": silent,
|
||||
"add_time": added_time,
|
||||
"status": TimerTaskStatus.PENDING,
|
||||
"executed": False
|
||||
}
|
||||
|
||||
|
||||
@Slot(int)
|
||||
def onTimerTypeComboBoxIndexChanged(
|
||||
self,
|
||||
index: int
|
||||
):
|
||||
|
||||
self.SpecificTimerWidget.setVisible(index == 0)
|
||||
self.RelativeTimerWidget.setVisible(index == 1)
|
||||
@@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALAddTimerTaskDialog</class>
|
||||
<widget class="QDialog" name="ALAddTimerTaskDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>300</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>添加定时任务 - AutoLibrary</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TaskNameLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="TaskNameLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>60</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>任务名称:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="TaskNameLineEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="TimerConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>定时设置</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="TimerConfigLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="TimerTypeLabel">
|
||||
<property name="text">
|
||||
<string>定时类型:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="TimerTypeComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>特定时间</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>相对时间</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="TaskConfigGroupBox">
|
||||
<property name="title">
|
||||
<string>运行设置</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="TaskConfigLayout">
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QRadioButton" name="SilentlyRunRadioButton">
|
||||
<property name="text">
|
||||
<string>静默运行</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoRepeat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
|
||||
<property name="text">
|
||||
<string>运行前提示</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ControLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="ControlSpaceFrame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="CancelButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>取消</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ConfirmButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>添加</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,419 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QMenu, QSystemTrayIcon
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
|
||||
)
|
||||
|
||||
from gui.Ui_ALMainWindow import Ui_ALMainWindow
|
||||
from gui.ALConfigWidget import ALConfigWidget
|
||||
from gui.ALTimerTaskWidget import ALTimerTaskWidget
|
||||
from gui.ALAboutDialog import ALAboutDialog
|
||||
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
|
||||
|
||||
from gui import AutoLibraryResource
|
||||
|
||||
from utils.ConfigReader import ConfigReader
|
||||
from utils.ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
timerTaskIsRunning = Signal(dict)
|
||||
timerTaskIsExecuted = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.__class_name = self.__class__.__name__
|
||||
|
||||
self.setupUi(self)
|
||||
self.__input_queue = queue.Queue()
|
||||
self.__output_queue = queue.Queue()
|
||||
self.__timer_task_queue = queue.Queue()
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
self.__config_paths = {
|
||||
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
|
||||
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
|
||||
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
|
||||
}
|
||||
self.__alTimerTaskWidget = None
|
||||
self.__alConfigWidget = None
|
||||
self.__auto_lib_thread = None
|
||||
self.__current_timer_task_thread = None
|
||||
self.__is_running_timer_task = False
|
||||
|
||||
self.modifyUi()
|
||||
self.setupTray()
|
||||
self.connectSignals()
|
||||
self.startMsgPolling()
|
||||
self.startTimerTaskPolling()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
|
||||
self.setWindowIcon(self.icon)
|
||||
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
||||
self.ManualAction.triggered.connect(self.onManualActionTriggered)
|
||||
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
|
||||
|
||||
# initialize timer task widget, but not show it
|
||||
self.__alTimerTaskWidget = ALTimerTaskWidget(self, self.__config_paths["timer_task"])
|
||||
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
|
||||
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
|
||||
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
|
||||
self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed)
|
||||
self.__alTimerTaskWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
|
||||
|
||||
|
||||
def onAboutActionTriggered(
|
||||
self
|
||||
):
|
||||
|
||||
about_dialog = ALAboutDialog(self)
|
||||
about_dialog.exec()
|
||||
|
||||
|
||||
def onManualActionTriggered(
|
||||
self
|
||||
):
|
||||
|
||||
url = QUrl("https://www.autolibrary.cv/docs/manual_lists.html")
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
|
||||
def setupTray(
|
||||
self
|
||||
):
|
||||
|
||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||
self.showTraceSignal.emit(
|
||||
"系统不支持系统托盘功能, 无法创建系统托盘图标。"
|
||||
)
|
||||
return
|
||||
self.TrayIcon = QSystemTrayIcon(self.icon, self)
|
||||
self.TrayIcon.setToolTip("AutoLibrary")
|
||||
|
||||
self.TrayMenu = QMenu()
|
||||
self.TrayMenu.addAction("显示主窗口", self.showNormal)
|
||||
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked)
|
||||
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
|
||||
self.TrayMenu.addSeparator()
|
||||
self.TrayMenu.addAction("退出", self.close)
|
||||
self.TrayIcon.setContextMenu(self.TrayMenu)
|
||||
|
||||
self.TrayIcon.setContextMenu(self.TrayMenu)
|
||||
self.TrayIcon.activated.connect(self.onTrayIconActivated)
|
||||
self.TrayIcon.show()
|
||||
|
||||
|
||||
def hideToTray(
|
||||
self
|
||||
):
|
||||
|
||||
self.hide()
|
||||
self.TrayIcon.showMessage(
|
||||
"AutoLibrary",
|
||||
"\n已最小化到托盘",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
2000
|
||||
)
|
||||
|
||||
|
||||
def onTrayIconActivated(
|
||||
self,
|
||||
reason: QSystemTrayIcon.ActivationReason
|
||||
):
|
||||
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
self.showNormal()
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
|
||||
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked)
|
||||
self.StartButton.clicked.connect(self.onStartButtonClicked)
|
||||
self.StopButton.clicked.connect(self.onStopButtonClicked)
|
||||
self.SendButton.clicked.connect(self.onSendButtonClicked)
|
||||
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
|
||||
self.__msg_queue_timer.stop()
|
||||
if self.__timer_task_timer and self.__timer_task_timer.isActive():
|
||||
self.__timer_task_timer.stop()
|
||||
if self.__is_running_timer_task:
|
||||
self.__current_timer_task_thread.wait(2000)
|
||||
self.__current_timer_task_thread.deleteLater()
|
||||
if self.__alTimerTaskWidget:
|
||||
self.__alTimerTaskWidget.close()
|
||||
self.__alTimerTaskWidget.deleteLater()
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.close()
|
||||
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def appendToTextEdit(
|
||||
self,
|
||||
text: str
|
||||
):
|
||||
|
||||
cursor = self.MessageIOTextEdit.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.insertText(text + "\n")
|
||||
self.MessageIOTextEdit.setTextCursor(cursor)
|
||||
self.MessageIOTextEdit.ensureCursorVisible()
|
||||
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
|
||||
def startMsgPolling(
|
||||
self
|
||||
):
|
||||
|
||||
self.__msg_queue_timer = QTimer()
|
||||
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
|
||||
self.__msg_queue_timer.start(100)
|
||||
|
||||
|
||||
def startTimerTaskPolling(
|
||||
self
|
||||
):
|
||||
|
||||
self.__timer_task_timer = QTimer()
|
||||
self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
|
||||
self.__timer_task_timer.start(500)
|
||||
|
||||
|
||||
def pollTimerTaskQueue(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__is_running_timer_task:
|
||||
return
|
||||
try:
|
||||
while not self.__is_running_timer_task:
|
||||
timer_task = self.__timer_task_queue.get_nowait()
|
||||
self.timerTaskIsRunning.emit(timer_task)
|
||||
self.__timer_task_timer.stop()
|
||||
self.__is_running_timer_task = True
|
||||
self.setControlButtons(False, False, True)
|
||||
if not timer_task["silent"]:
|
||||
self.TrayIcon.showMessage(
|
||||
"定时任务 - AutoLibrary",
|
||||
f"\n已开始执行定时任务: \n{timer_task['name']}",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
1000
|
||||
)
|
||||
self.showNormal()
|
||||
self.__current_timer_task_thread = TimerTaskWorker(
|
||||
timer_task,
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished)
|
||||
self.__current_timer_task_thread.showTraceSignal.connect(self.showTrace)
|
||||
self.__current_timer_task_thread.showMsgSignal.connect(self.showMsg)
|
||||
self.__current_timer_task_thread.start()
|
||||
except queue.Empty:
|
||||
self.__is_running_timer_task = False
|
||||
pass
|
||||
|
||||
|
||||
def setControlButtons(
|
||||
self,
|
||||
config_button_enabled: bool,
|
||||
stop_button_enabled: bool,
|
||||
start_button_enabled: bool
|
||||
):
|
||||
|
||||
self.ConfigButton.setEnabled(config_button_enabled)
|
||||
self.StopButton.setEnabled(stop_button_enabled)
|
||||
self.StartButton.setEnabled(start_button_enabled)
|
||||
|
||||
@Slot()
|
||||
def showMsg(
|
||||
self,
|
||||
msg: str
|
||||
):
|
||||
|
||||
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
|
||||
|
||||
@Slot()
|
||||
def showTrace(
|
||||
self,
|
||||
msg: str
|
||||
):
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
|
||||
|
||||
@Slot()
|
||||
def pollMsgQueue(
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = self.__output_queue.get_nowait()
|
||||
self.appendToTextEdit(msg)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskWidgetClosed(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTaskWidgetButton.setEnabled(True)
|
||||
|
||||
|
||||
@Slot(dict)
|
||||
def onConfigWidgetClosed(
|
||||
self,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.deleteLater()
|
||||
self.__alConfigWidget = None
|
||||
self.setControlButtons(True, False, True)
|
||||
self.__config_paths = config_paths
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsReady(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
self.__timer_task_queue.put(timer_task)
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskFinished(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
self.__current_timer_task_thread.wait(1000)
|
||||
self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished)
|
||||
self.__current_timer_task_thread.showTraceSignal.disconnect(self.showTrace)
|
||||
self.__current_timer_task_thread.showMsgSignal.disconnect(self.showMsg)
|
||||
self.__current_timer_task_thread.deleteLater()
|
||||
self.__current_timer_task_thread = None
|
||||
self.setControlButtons(True, False, True)
|
||||
self.__is_running_timer_task = False
|
||||
self.__timer_task_timer.start(500)
|
||||
timer_task["executed"] = True
|
||||
self.TrayIcon.showMessage(
|
||||
"定时任务 - AutoLibrary",
|
||||
f"\n定时任务 '{timer_task['name']}' 执行完成",
|
||||
QSystemTrayIcon.MessageIcon.Information,
|
||||
1000
|
||||
)
|
||||
self.showTrace(f"定时任务 {timer_task['name']} 执行完成, uuid: {timer_task['task_uuid']}")
|
||||
self.timerTaskIsExecuted.emit(timer_task)
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskWidgetButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.__alTimerTaskWidget.show()
|
||||
self.__alTimerTaskWidget.raise_()
|
||||
self.__alTimerTaskWidget.activateWindow()
|
||||
self.TimerTaskWidgetButton.setEnabled(False)
|
||||
|
||||
@Slot()
|
||||
def onConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__alConfigWidget is None:
|
||||
self.__alConfigWidget = ALConfigWidget(
|
||||
self,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.show()
|
||||
self.__alConfigWidget.raise_()
|
||||
self.__alConfigWidget.activateWindow()
|
||||
self.ConfigButton.setEnabled(False)
|
||||
|
||||
@Slot()
|
||||
def onStartButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.setControlButtons(False, False, True)
|
||||
if self.__auto_lib_thread is None:
|
||||
self.__auto_lib_thread = AutoLibWorker(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
|
||||
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
|
||||
self.__auto_lib_thread.start()
|
||||
|
||||
@Slot()
|
||||
def onStopButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__auto_lib_thread:
|
||||
self.showTrace("正在停止操作......")
|
||||
self.__auto_lib_thread.wait(2000)
|
||||
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, False, True)
|
||||
|
||||
@Slot()
|
||||
def onSendButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
msg = self.MessageEdit.text().strip()
|
||||
if not msg:
|
||||
return
|
||||
self.showMsg(msg)
|
||||
self.MessageEdit.clear()
|
||||
@@ -50,11 +50,33 @@
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="TimerTaskWidgetButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="document-open-recent"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="ControlSpaceFrame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1280</width>
|
||||
<width>1000</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
@@ -237,6 +259,9 @@ font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
<property name="text">
|
||||
<string>发送</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="document-send"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@@ -245,7 +270,7 @@ font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="MenuBar">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
@@ -258,12 +283,33 @@ font: 700 9pt "Microsoft YaHei UI";</string>
|
||||
<property name="nativeMenuBar">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QMenu" name="HelpMenu">
|
||||
<property name="mouseTracking">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>帮助</string>
|
||||
</property>
|
||||
<addaction name="ManualAction"/>
|
||||
<addaction name="AboutAction"/>
|
||||
</widget>
|
||||
<addaction name="HelpMenu"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="StatusBar">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<action name="ManualAction">
|
||||
<property name="text">
|
||||
<string>在线手册</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="AboutAction">
|
||||
<property name="text">
|
||||
<string>关于</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Signal, QThread
|
||||
)
|
||||
|
||||
from operators.AutoLib import AutoLib
|
||||
from utils.ConfigReader import ConfigReader
|
||||
|
||||
|
||||
class AutoLibWorker(QThread):
|
||||
|
||||
finishedSignal = Signal()
|
||||
showTraceSignal = Signal(str)
|
||||
showMsgSignal = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.__input_queue = input_queue
|
||||
self.__output_queue = output_queue
|
||||
self.__config_paths = config_paths
|
||||
|
||||
|
||||
def checkTimeAvailable(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
current_time = time.strftime("%H:%M", time.localtime())
|
||||
if current_time >= "23:30" or current_time <= "07:30":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def checkConfigPaths(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
if not all(
|
||||
os.path.exists(path) for path in self.__config_paths.values()
|
||||
):
|
||||
self.showTraceSignal.emit(
|
||||
"配置文件路径不存在, 请检查配置文件路径是否正确。"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def loadConfigs(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
self.showTraceSignal.emit(
|
||||
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
|
||||
)
|
||||
self.__run_config = ConfigReader(
|
||||
self.__config_paths["run"]
|
||||
).getConfigs()
|
||||
self.showTraceSignal.emit(
|
||||
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
|
||||
)
|
||||
self.__user_config = ConfigReader(
|
||||
self.__config_paths["user"]
|
||||
).getConfigs()
|
||||
if self.__run_config is None or self.__user_config is None:
|
||||
self.showTraceSignal.emit(
|
||||
"配置文件加载失败, 请检查配置文件是否正确。"
|
||||
)
|
||||
return False
|
||||
if not self.__user_config.get("groups"):
|
||||
self.showTraceSignal.emit(
|
||||
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
|
||||
auto_lib = None
|
||||
try:
|
||||
if not self.checkTimeAvailable():
|
||||
self.showTraceSignal.emit(
|
||||
"当前时间不在图书馆开放时间内\n"\
|
||||
" 请在 07:30 - 23:30 之间尝试"
|
||||
)
|
||||
return
|
||||
if not self.checkConfigPaths():
|
||||
return
|
||||
self.showTraceSignal.emit("AutoLibrary 开始运行")
|
||||
if not self.loadConfigs():
|
||||
return
|
||||
auto_lib = AutoLib(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
self.__run_config
|
||||
)
|
||||
if auto_lib is None:
|
||||
self.showTraceSignal.emit(
|
||||
"AutoLibrary 初始化失败"
|
||||
)
|
||||
return
|
||||
groups = self.__user_config.get("groups")
|
||||
for group in groups:
|
||||
time.sleep(0.2) # wait for the message queue to be empty
|
||||
if not group["enabled"]:
|
||||
self.showTraceSignal.emit(
|
||||
f"任务组 {group["name"]} 已跳过"
|
||||
)
|
||||
continue
|
||||
self.showTraceSignal.emit(
|
||||
f"正在运行任务组 {group["name"]}"
|
||||
)
|
||||
auto_lib.run(
|
||||
{ "users": group.get("users", []) }
|
||||
)
|
||||
except Exception as e:
|
||||
self.showTraceSignal.emit(
|
||||
f"AutoLibrary 运行时发生异常 : {e}"
|
||||
)
|
||||
finally:
|
||||
if auto_lib:
|
||||
auto_lib.close()
|
||||
time.sleep(0.2) # wait for the message queue to be empty
|
||||
self.showTraceSignal.emit("AutoLibrary 运行结束")
|
||||
self.finishedSignal.emit()
|
||||
|
||||
|
||||
class TimerTaskWorker(AutoLibWorker):
|
||||
|
||||
finishedSignal_TimerWorker = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timer_task: dict,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue, config_paths)
|
||||
|
||||
self.__timer_task = timer_task
|
||||
|
||||
def run(
|
||||
self
|
||||
):
|
||||
|
||||
self.showTraceSignal.emit(
|
||||
f"定时任务 {self.__timer_task['name']} 开始运行"
|
||||
)
|
||||
super().run()
|
||||
self.showTraceSignal.emit(
|
||||
f"定时任务 {self.__timer_task['name']} 运行结束"
|
||||
)
|
||||
self.finishedSignal_TimerWorker.emit(self.__timer_task)
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QLabel
|
||||
)
|
||||
|
||||
|
||||
class ALSeatFrame(QFrame):
|
||||
|
||||
clicked = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
seat_number,
|
||||
parent=None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__seat_number = seat_number
|
||||
self.__is_selected = False
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setFixedSize(60, 40)
|
||||
self.setFrameStyle(QFrame.Box | QFrame.Plain)
|
||||
self.setLineWidth(2)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.label = QLabel(self.__seat_number, self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.label.setGeometry(0, 0, 60, 40)
|
||||
|
||||
def mousePressEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.toggleSelection()
|
||||
self.clicked.emit(self.__seat_number)
|
||||
|
||||
|
||||
def isSelected(
|
||||
self
|
||||
):
|
||||
|
||||
return self.__is_selected
|
||||
|
||||
|
||||
def toggleSelection(self):
|
||||
|
||||
self.__is_selected = not self.__is_selected
|
||||
if self.__is_selected:
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #388E3C;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
@@ -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,276 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from PySide6.QtCore import (
|
||||
Qt, Slot, Signal, QEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout,
|
||||
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem,
|
||||
QPushButton,
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QPainter, QWheelEvent, QCloseEvent
|
||||
)
|
||||
from gui.ALSeatFrame import ALSeatFrame
|
||||
|
||||
|
||||
class ALSeatMapWidget(QWidget):
|
||||
|
||||
seatMapWidgetClosed = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget = None,
|
||||
floor: str = "",
|
||||
room: str = "",
|
||||
seats_data: dict = {},
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.__floor = floor
|
||||
self.__room = room
|
||||
self.__seats_data = seats_data
|
||||
self.__selected_seats = []
|
||||
self.__seat_frames = {}
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
@staticmethod
|
||||
def formatSeatNumber(
|
||||
seat_number: str
|
||||
) -> str:
|
||||
|
||||
if seat_number and not seat_number[-1].isdigit():
|
||||
digits = seat_number[:-1]
|
||||
letter = seat_number[-1]
|
||||
return digits.zfill(3) + letter
|
||||
return seat_number.zfill(3)
|
||||
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowFlags(Qt.WindowType.Window)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(800, 600)
|
||||
self.resize(800, 600)
|
||||
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
|
||||
|
||||
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
|
||||
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
|
||||
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
|
||||
|
||||
self.SeatMapGraphicsView = QGraphicsView(self)
|
||||
self.SeatMapGraphicsScene = QGraphicsScene(self)
|
||||
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
|
||||
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
|
||||
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
||||
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.viewport().installEventFilter(self)
|
||||
|
||||
self.SeatsContainerWidget = QWidget()
|
||||
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
|
||||
self.createSeatMap()
|
||||
|
||||
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
|
||||
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
|
||||
|
||||
self.TipsLabel = QLabel(
|
||||
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
|
||||
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
|
||||
)
|
||||
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
|
||||
|
||||
self.ConfirmButton = QPushButton("确认")
|
||||
self.ConfirmButton.setFixedSize(80, 25)
|
||||
self.CancelButton = QPushButton("取消")
|
||||
self.CancelButton.setFixedSize(80, 25)
|
||||
self.SeatMapWidgetControlLayout = QHBoxLayout()
|
||||
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
|
||||
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
|
||||
def showEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
result = super().showEvent(event)
|
||||
|
||||
screen_rect = self.screen().geometry()
|
||||
target_pos = self.parent().geometry().center()
|
||||
target_pos.setX(target_pos.x() - self.width()//2)
|
||||
target_pos.setY(target_pos.y() - self.height()//2)
|
||||
if target_pos.x() < 0:
|
||||
target_pos.setX(0)
|
||||
if target_pos.x() + self.width() > screen_rect.width():
|
||||
target_pos.setX(screen_rect.width() - self.width())
|
||||
if target_pos.y() < 0:
|
||||
target_pos.setY(0)
|
||||
if target_pos.y() + self.height() > screen_rect.height():
|
||||
target_pos.setY(screen_rect.height() - self.height())
|
||||
self.move(target_pos)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.seatMapWidgetClosed.emit(self.__selected_seats)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def eventFilter(
|
||||
self,
|
||||
watched,
|
||||
event
|
||||
):
|
||||
|
||||
if (watched is self.SeatMapGraphicsView.viewport() and
|
||||
event.type() == QEvent.Type.Wheel and
|
||||
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||||
):
|
||||
self.zoomGraphicsView(event)
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
|
||||
def zoomGraphicsView(
|
||||
self,
|
||||
event: QWheelEvent
|
||||
):
|
||||
|
||||
delta = event.angleDelta().y()
|
||||
zoom_factor = 1.2 if delta > 0 else 1/1.2
|
||||
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor)
|
||||
|
||||
|
||||
def createSeatMap(
|
||||
self
|
||||
):
|
||||
|
||||
rows = self.__seats_data.strip().split("\n")
|
||||
for row_idx, row in enumerate(rows):
|
||||
col_idx = 0
|
||||
seats_number = [seat.strip() for seat in row.split(",")]
|
||||
for seat_number in seats_number:
|
||||
if seat_number:
|
||||
seat_widget = ALSeatFrame(seat_number)
|
||||
seat_widget.clicked.connect(self.onSeatClicked)
|
||||
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
|
||||
self.__seat_frames[seat_number] = seat_widget
|
||||
else:
|
||||
spacer = QFrame()
|
||||
spacer.setFixedSize(20, 30)
|
||||
spacer.setStyleSheet("background-color: transparent; border: none;")
|
||||
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
|
||||
col_idx += 1
|
||||
self.SeatsContainerLayout.setSpacing(20)
|
||||
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
|
||||
self.SeatsContainerWidget.adjustSize()
|
||||
|
||||
|
||||
def selectSeat(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if len(self.__selected_seats) >= 1:
|
||||
return
|
||||
seat_number = self.formatSeatNumber(seat_number)
|
||||
if seat_number not in self.__seat_frames:
|
||||
return
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
return
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats.append(seat_number)
|
||||
|
||||
|
||||
def selectSeats(
|
||||
self,
|
||||
selected_seats: list
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
for seat_number in selected_seats:
|
||||
self.selectSeat(seat_number)
|
||||
|
||||
|
||||
def getSelectedSeats(
|
||||
self
|
||||
) -> list[str]:
|
||||
|
||||
return self.__selected_seats
|
||||
|
||||
|
||||
def clearSelections(
|
||||
self
|
||||
):
|
||||
|
||||
seats_to_clear = self.__selected_seats.copy()
|
||||
for seat_number in seats_to_clear:
|
||||
if seat_number not in self.__seat_frames:
|
||||
continue
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats = []
|
||||
|
||||
@Slot(str)
|
||||
def onSeatClicked(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if seat_number in self.__selected_seats:
|
||||
self.__selected_seats.remove(seat_number)
|
||||
else:
|
||||
if len(self.__selected_seats) < 1:
|
||||
self.__selected_seats.append(seat_number)
|
||||
else:
|
||||
self.__seat_frames[seat_number].toggleSelection()
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
self.close()
|
||||
@@ -0,0 +1,472 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import copy
|
||||
import queue
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QTimer
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QWidget, QListWidgetItem, QMessageBox,
|
||||
QHBoxLayout, QVBoxLayout, QLabel, QPushButton
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QCloseEvent, QScreen
|
||||
)
|
||||
|
||||
from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget
|
||||
from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus
|
||||
|
||||
from utils.ConfigReader import ConfigReader
|
||||
from utils.ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class SortPolicy(Enum):
|
||||
|
||||
BY_NAME = "按名称"
|
||||
BY_ADD_TIME = "按添加时间"
|
||||
BY_EXECUTE_TIME = "按执行时间"
|
||||
|
||||
|
||||
class TimerTaskItemWidget(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
timer_task: dict = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.__timer_task = timer_task
|
||||
self.modifyUi()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.ItemWidgetLayout = QHBoxLayout(self)
|
||||
self.ItemWidgetLayout.setSpacing(10)
|
||||
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
|
||||
|
||||
self.TaskInfoLayout = QVBoxLayout()
|
||||
self.TaskInfoLayout.setSpacing(5)
|
||||
TaskNameLabel = QLabel(self.__timer_task["name"])
|
||||
TaskNameLabelFont = TaskNameLabel.font()
|
||||
TaskNameLabelFont.setBold(True)
|
||||
TaskNameLabel.setFont(TaskNameLabelFont)
|
||||
TaskNameLabel.setFixedHeight(25)
|
||||
self.TaskInfoLayout.addWidget(TaskNameLabel)
|
||||
|
||||
ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
|
||||
ExecuteTimeLabel.setStyleSheet("color: gray;")
|
||||
ExecuteTimeLabel.setFixedHeight(20)
|
||||
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
|
||||
|
||||
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
|
||||
self.ItemWidgetLayout.addStretch()
|
||||
|
||||
match self.__timer_task["status"]:
|
||||
case TimerTaskStatus.PENDING:
|
||||
TaskStatusText = "等待中"
|
||||
TaskStatusColor = "#FF9800"
|
||||
case TimerTaskStatus.READY:
|
||||
TaskStatusText = "已就绪"
|
||||
TaskStatusColor = "#316BFF"
|
||||
case TimerTaskStatus.RUNNING:
|
||||
TaskStatusText = "执行中"
|
||||
TaskStatusColor = "#2294FF"
|
||||
case TimerTaskStatus.EXECUTED:
|
||||
TaskStatusText = "已执行"
|
||||
TaskStatusColor = "#4CAF50"
|
||||
case TimerTaskStatus.OUTDATED:
|
||||
TaskStatusText = "已过期"
|
||||
TaskStatusColor = "#FF5722"
|
||||
TaskStatusLabel = QLabel(TaskStatusText)
|
||||
TaskStatusLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: {TaskStatusColor};
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
TaskStatusLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
TaskStatusLabel.setFixedSize(80, 25)
|
||||
self.ItemWidgetLayout.addWidget(TaskStatusLabel)
|
||||
|
||||
TaskModeText = "静默" if self.__timer_task["silent"] else "显示"
|
||||
TaskModeColor = "#6325FF" if self.__timer_task["silent"] else "#2294FF"
|
||||
TaskModeLabel = QLabel(TaskModeText)
|
||||
TaskModeLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: {TaskModeColor};
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
TaskModeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
TaskModeLabel.setFixedSize(60, 25)
|
||||
self.ItemWidgetLayout.addWidget(TaskModeLabel)
|
||||
|
||||
self.DeleteButton = QPushButton("删除")
|
||||
self.DeleteButton.setFixedSize(80, 25)
|
||||
self.ItemWidgetLayout.addWidget(self.DeleteButton)
|
||||
if self.__timer_task["status"] == TimerTaskStatus.READY\
|
||||
or self.__timer_task["status"] == TimerTaskStatus.RUNNING:
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.setFixedHeight(55)
|
||||
|
||||
|
||||
class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
|
||||
|
||||
timerTasksChanged = Signal()
|
||||
timerTaskIsReady = Signal(dict)
|
||||
timerTaskWidgetClosed = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
timer_tasks_config_path: str = ""
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.__timer_tasks = []
|
||||
self.__check_timer = None
|
||||
self.__sort_policy = SortPolicy.BY_EXECUTE_TIME
|
||||
self.__timer_tasks_config_path = timer_tasks_config_path
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.setupTimer()
|
||||
if not self.initializeTimerTasks():
|
||||
return
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.AddTimerTaskButton.clicked.connect(self.addTask)
|
||||
self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks)
|
||||
self.TimerTaskSortTypeComboBox.currentIndexChanged.connect(self.onSortPolicyComboBoxChanged)
|
||||
self.timerTasksChanged.connect(self.onTimerTasksChanged)
|
||||
|
||||
|
||||
def setupTimer(
|
||||
self
|
||||
):
|
||||
|
||||
self.__check_timer = QTimer(self)
|
||||
self.__check_timer.timeout.connect(self.checkTasks)
|
||||
self.__check_timer.start(500)
|
||||
|
||||
|
||||
def initializeTimerTasks(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
timer_tasks = self.loadTimerTasks(self.__timer_tasks_config_path)
|
||||
if timer_tasks is not None:
|
||||
self.__timer_tasks = timer_tasks
|
||||
self.timerTasksChanged.emit()
|
||||
return True
|
||||
timer_tasks = []
|
||||
if self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(timer_tasks)):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
f"定时任务配置文件初始化完成: \n{self.__timer_tasks_config_path}"
|
||||
)
|
||||
self.__timer_tasks = timer_tasks
|
||||
self.updateTimerTaskList()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def loadTimerTasks(
|
||||
self,
|
||||
timer_tasks_config_path: str
|
||||
) -> list:
|
||||
|
||||
try:
|
||||
if not timer_tasks_config_path or not os.path.exists(timer_tasks_config_path):
|
||||
raise Exception("定时任务配置文件不存在")
|
||||
timer_tasks = ConfigReader(timer_tasks_config_path).getConfigs()
|
||||
if timer_tasks and "timer_tasks" in timer_tasks:
|
||||
for task in timer_tasks["timer_tasks"]:
|
||||
task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = TimerTaskStatus(task["status"])
|
||||
return timer_tasks["timer_tasks"]
|
||||
raise Exception("定时任务配置文件格式错误")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"加载定时任务配置发生错误 ! : {e}\n"\
|
||||
f"文件路径: {timer_tasks_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveTimerTasks(
|
||||
self,
|
||||
timer_tasks_config_path: str,
|
||||
timer_tasks: list
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not timer_tasks_config_path:
|
||||
raise Exception("配置文件路径为空")
|
||||
for task in timer_tasks:
|
||||
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = task["status"].value
|
||||
ConfigWriter(
|
||||
timer_tasks_config_path,
|
||||
{ "timer_tasks": timer_tasks }
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"保存定时任务配置发生错误 ! : {e}\n"\
|
||||
f"文件路径: {timer_tasks_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def showEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
result = super().showEvent(event)
|
||||
|
||||
screen_rect = self.screen().geometry()
|
||||
target_pos = self.parent().geometry().center()
|
||||
target_pos.setX(target_pos.x() - self.width()//2)
|
||||
target_pos.setY(target_pos.y() - self.height()//2)
|
||||
if target_pos.x() < 0:
|
||||
target_pos.setX(0)
|
||||
if target_pos.x() + self.width() > screen_rect.width():
|
||||
target_pos.setX(screen_rect.width() - self.width())
|
||||
if target_pos.y() < 0:
|
||||
target_pos.setY(0)
|
||||
if target_pos.y() + self.height() > screen_rect.height():
|
||||
target_pos.setY(screen_rect.height() - self.height())
|
||||
self.move(target_pos)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.hide()
|
||||
self.timerTaskWidgetClosed.emit()
|
||||
event.ignore()
|
||||
|
||||
|
||||
def sortTimerTasks(
|
||||
self,
|
||||
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME
|
||||
):
|
||||
|
||||
if policy == SortPolicy.BY_NAME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["name"]
|
||||
)
|
||||
elif policy == SortPolicy.BY_ADD_TIME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["add_time"]
|
||||
)
|
||||
elif policy == SortPolicy.BY_EXECUTE_TIME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["execute_time"]
|
||||
)
|
||||
|
||||
|
||||
def updateStat(
|
||||
self
|
||||
):
|
||||
|
||||
pending = 0
|
||||
in_queue = 0
|
||||
executed = 0
|
||||
total = len(self.__timer_tasks)
|
||||
for timer_task in self.__timer_tasks:
|
||||
if timer_task["status"] == TimerTaskStatus.PENDING:
|
||||
pending += 1
|
||||
elif timer_task["status"] == TimerTaskStatus.READY\
|
||||
or timer_task["status"] == TimerTaskStatus.RUNNING:
|
||||
in_queue += 1
|
||||
elif timer_task["status"] == TimerTaskStatus.EXECUTED:
|
||||
executed += 1
|
||||
self.TotalTaskLabel.setText(f"总任务:{total}")
|
||||
self.PendingTaskLabel.setText(f"待执行:{pending}")
|
||||
self.InQueueTaskLabel.setText(f"队列中:{in_queue}")
|
||||
self.ExecutedTaskLabel.setText(f"已执行:{executed}")
|
||||
|
||||
|
||||
def updateTimerTaskList(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTasksListWidget.clear()
|
||||
self.sortTimerTasks(self.__sort_policy)
|
||||
for timer_task in self.__timer_tasks:
|
||||
item = QListWidgetItem()
|
||||
item.setData(Qt.UserRole, timer_task)
|
||||
widget = TimerTaskItemWidget(self, timer_task)
|
||||
widget.DeleteButton.clicked.connect(
|
||||
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid)
|
||||
)
|
||||
item.setSizeHint(widget.size())
|
||||
self.TimerTasksListWidget.addItem(item)
|
||||
self.TimerTasksListWidget.setItemWidget(item, widget)
|
||||
|
||||
|
||||
def addTask(
|
||||
self
|
||||
):
|
||||
|
||||
dialog = ALAddTimerTaskWidget(self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
timer_task = dialog.getTimerTask()
|
||||
self.__timer_tasks.append(timer_task)
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
def deleteTask(
|
||||
self,
|
||||
task_uuid: str
|
||||
):
|
||||
|
||||
self.__timer_tasks = [
|
||||
x for x in self.__timer_tasks
|
||||
if x["task_uuid"] != task_uuid
|
||||
]
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
def clearAllTasks(
|
||||
self
|
||||
):
|
||||
|
||||
if not self.__timer_tasks:
|
||||
return
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认 - AutoLibrary",
|
||||
"是否要清除所有定时任务 ?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if result is QMessageBox.StandardButton.No:
|
||||
return
|
||||
in_queue_tasks = [
|
||||
x for x in self.__timer_tasks
|
||||
if x["status"] == TimerTaskStatus.READY
|
||||
or x["status"] == TimerTaskStatus.RUNNING
|
||||
]
|
||||
in_queue_count = len(in_queue_tasks)
|
||||
if in_queue_count > 0:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !"
|
||||
)
|
||||
self.__timer_tasks = in_queue_tasks
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
def checkTasks(
|
||||
self
|
||||
):
|
||||
|
||||
need_update = False
|
||||
|
||||
now = datetime.now()
|
||||
for timer_task in self.__timer_tasks:
|
||||
if timer_task["execute_time"] > now:
|
||||
continue
|
||||
if timer_task["status"] is not TimerTaskStatus.PENDING:
|
||||
continue
|
||||
if timer_task["execute_time"] <= now + timedelta(seconds = -5):
|
||||
timer_task["status"] = TimerTaskStatus.OUTDATED
|
||||
need_update = True
|
||||
else:
|
||||
timer_task["status"] = TimerTaskStatus.READY
|
||||
self.timerTaskIsReady.emit(timer_task)
|
||||
need_update = True
|
||||
if need_update:
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
@Slot(int)
|
||||
def onSortPolicyComboBoxChanged(
|
||||
self,
|
||||
policy: int
|
||||
):
|
||||
|
||||
mapping = {
|
||||
0: SortPolicy.BY_NAME,
|
||||
1: SortPolicy.BY_ADD_TIME,
|
||||
2: SortPolicy.BY_EXECUTE_TIME
|
||||
}
|
||||
self.__sort_policy = mapping[policy]
|
||||
self.updateTimerTaskList()
|
||||
|
||||
|
||||
@Slot()
|
||||
def onTimerTasksChanged(
|
||||
self
|
||||
):
|
||||
|
||||
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks))
|
||||
self.updateTimerTaskList()
|
||||
self.updateStat()
|
||||
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsRunning(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
task["status"] = TimerTaskStatus.RUNNING
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsExecuted(
|
||||
self,
|
||||
timer_task: dict
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
task["status"] = TimerTaskStatus.EXECUTED
|
||||
self.timerTasksChanged.emit()
|
||||
@@ -0,0 +1,315 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALTimerTaskWidget</class>
|
||||
<widget class="QWidget" name="ALTimerTaskWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>400</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>定时任务 - AutoLibrary</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TimerTaskStatusLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="TotalTaskLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>总任务:0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="PendingTaskLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel {
|
||||
color: #FF9800
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>待执行:0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="InQueueTaskLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel {
|
||||
color: #2294FF
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>队列中:0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="ExecutedTaskLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel {
|
||||
color: #4CAF50
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>已执行:0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="TimerTaskSpaceFrame">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TimerTaskSortLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="TimerTaskSortSpaceFrame">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="TimerTaskSortLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>排序:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="TimerTaskSortTypeComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>90</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>90</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>按名称</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>按添加时间</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>按执行时间</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="TimerTasksListWidget">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="TimerTaskEditLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ClearAllTimerTasksButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>清除全部</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="AddTimerTaskButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>添加任务</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="TimerTaskEditSpaceFrame">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, QSize, QCoreApplication, QRect, QPoint
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractScrollArea, QAbstractItemView,
|
||||
QTreeWidget, QTreeWidgetItem
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QDragEnterEvent, QDragMoveEvent, QDropEvent
|
||||
)
|
||||
|
||||
|
||||
class TreeItemType(Enum):
|
||||
|
||||
GROUP = 0
|
||||
USER = 1
|
||||
|
||||
|
||||
class ALUserTreeWidget(QTreeWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi()
|
||||
self.translateUi()
|
||||
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
__qtreewidgetitem = QTreeWidgetItem()
|
||||
__qtreewidgetitem.setText(0, u"\u5206\u7ec4/\u7528\u6237");
|
||||
self.setHeaderItem(__qtreewidgetitem)
|
||||
self.setObjectName(u"UserTreeWidget")
|
||||
self.setMinimumSize(QSize(230, 0))
|
||||
self.setMaximumSize(QSize(250, 16777215))
|
||||
self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored)
|
||||
self.setTabKeyNavigation(True)
|
||||
self.setDragEnabled(True)
|
||||
self.setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||
self.setDefaultDropAction(Qt.DropAction.IgnoreAction)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSortingEnabled(True)
|
||||
self.setAnimated(True)
|
||||
self.setAllColumnsShowFocus(False)
|
||||
self.setHeaderHidden(False)
|
||||
self.setColumnCount(2)
|
||||
self.setColumnWidth(0, 150)
|
||||
self.setColumnWidth(1, 20)
|
||||
self.header().setCascadingSectionResizes(False)
|
||||
self.header().setHighlightSections(False)
|
||||
self.header().setProperty(u"showSortIndicator", True)
|
||||
|
||||
|
||||
def translateUi(
|
||||
self
|
||||
):
|
||||
|
||||
___qtreewidgetitem = self.headerItem()
|
||||
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
|
||||
|
||||
|
||||
@staticmethod
|
||||
def isDragPositionValid(
|
||||
target_rect: QRect,
|
||||
drag_pos: QPoint,
|
||||
) -> bool:
|
||||
|
||||
y_offset = drag_pos.y() - target_rect.top()
|
||||
valid = (y_offset > target_rect.height()*0.2 and
|
||||
y_offset < target_rect.height()*0.8)
|
||||
return valid
|
||||
|
||||
|
||||
def dragEnterEvent(
|
||||
self,
|
||||
event: QDragEnterEvent
|
||||
):
|
||||
|
||||
super().dragEnterEvent(event)
|
||||
|
||||
|
||||
def dragMoveEvent(
|
||||
self,
|
||||
event: QDragMoveEvent
|
||||
):
|
||||
|
||||
super().dragMoveEvent(event)
|
||||
|
||||
source_item = self.currentItem()
|
||||
target_item = self.itemAt(event.position().toPoint())
|
||||
if source_item is None:
|
||||
event.ignore()
|
||||
return
|
||||
if source_item.type() == TreeItemType.GROUP.value:
|
||||
if target_item is not None:
|
||||
event.ignore()
|
||||
return
|
||||
elif source_item.type() == TreeItemType.USER.value:
|
||||
if target_item is None:
|
||||
event.ignore()
|
||||
return
|
||||
if target_item.type() != TreeItemType.GROUP.value:
|
||||
event.ignore()
|
||||
return
|
||||
if target_item.checkState(1) == Qt.CheckState.Unchecked:
|
||||
event.ignore()
|
||||
return
|
||||
if not self.isDragPositionValid(
|
||||
self.visualItemRect(target_item),
|
||||
event.position().toPoint()
|
||||
):
|
||||
event.ignore()
|
||||
return
|
||||
else:
|
||||
event.ignore()
|
||||
return
|
||||
event.acceptProposedAction()
|
||||
|
||||
|
||||
def dropEvent(
|
||||
self,
|
||||
event: QDropEvent
|
||||
):
|
||||
|
||||
super().dropEvent(event)
|
||||
|
||||
for item_index in range(self.topLevelItemCount()):
|
||||
self.topLevelItem(item_index).setExpanded(True)
|
||||
self.setCurrentItem(None)
|
||||
@@ -0,0 +1 @@
|
||||
AL_VERSION = "1.0.0-beta"
|
||||
|
Before Width: | Height: | Size: 785 KiB After Width: | Height: | Size: 785 KiB |
@@ -16,13 +16,15 @@ from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.edge.service import Service
|
||||
|
||||
from MsgBase import MsgBase
|
||||
from LibChecker import LibChecker
|
||||
from LibLogin import LibLogin
|
||||
from LibLogout import LibLogout
|
||||
from LibReserve import LibReserve
|
||||
from base.MsgBase import MsgBase
|
||||
from operators.LibChecker import LibChecker
|
||||
from operators.LibLogin import LibLogin
|
||||
from operators.LibLogout import LibLogout
|
||||
from operators.LibReserve import LibReserve
|
||||
from operators.LibCheckin import LibCheckin
|
||||
from operators.LibRenew import LibRenew
|
||||
|
||||
from ConfigReader import ConfigReader
|
||||
from utils.ConfigReader import ConfigReader
|
||||
|
||||
|
||||
class AutoLib(MsgBase):
|
||||
@@ -31,12 +33,19 @@ class AutoLib(MsgBase):
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
run_config: dict
|
||||
):
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__system_config_reader = None
|
||||
self.__users_config_reader = None
|
||||
self.__run_config = run_config
|
||||
self.__user_config = None
|
||||
self.__driver = None
|
||||
if not self.__initBrowserDriver():
|
||||
return None
|
||||
else:
|
||||
if not self.__initDriverUrl():
|
||||
return None
|
||||
self.__initLibOperators()
|
||||
|
||||
|
||||
def __initBrowserDriver(
|
||||
@@ -46,13 +55,18 @@ class AutoLib(MsgBase):
|
||||
self._showTrace("正在初始化浏览器驱动......")
|
||||
edge_options = webdriver.EdgeOptions()
|
||||
|
||||
if self.__system_config_reader.get("web_driver/headless"):
|
||||
web_driver_config = self.__run_config.get("web_driver", None)
|
||||
if not web_driver_config:
|
||||
self._showTrace("未配置浏览器驱动参数 !")
|
||||
return False
|
||||
if web_driver_config.get("headless"):
|
||||
edge_options.add_argument("--headless")
|
||||
edge_options.add_argument("--disable-gpu")
|
||||
edge_options.add_argument("--no-sandbox")
|
||||
edge_options.add_argument("--disable-dev-shm-usage")
|
||||
|
||||
edge_options.add_argument("--window-size=1280,720")
|
||||
# must be 1920x1080, otherwise the page will cause some elements not accessible
|
||||
edge_options.add_argument("--window-size=1920,1080")
|
||||
edge_options.add_argument("--remote-allow-origins=*")
|
||||
|
||||
# omit ssl errors and verbose log level
|
||||
@@ -73,8 +87,8 @@ class AutoLib(MsgBase):
|
||||
)
|
||||
|
||||
# init browser driver
|
||||
self.__driver_path = self.__system_config_reader.get("web_driver/driver_path")
|
||||
self.__driver_type = self.__system_config_reader.get("web_driver/driver_type")
|
||||
self.__driver_path = web_driver_config.get("driver_path")
|
||||
self.__driver_type = web_driver_config.get("driver_type")
|
||||
self.__driver_path = os.path.abspath(self.__driver_path)
|
||||
try:
|
||||
service = None
|
||||
@@ -89,7 +103,7 @@ class AutoLib(MsgBase):
|
||||
self.__driver = webdriver.Firefox(service=service, options=edge_options)
|
||||
case _:
|
||||
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
|
||||
self.__driver.implicitly_wait(10)
|
||||
self.__driver.implicitly_wait(1)
|
||||
self.__driver.execute_script(
|
||||
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
)
|
||||
@@ -111,15 +125,17 @@ 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)
|
||||
self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver)
|
||||
|
||||
|
||||
def __waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# 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
|
||||
@@ -144,7 +160,12 @@ class AutoLib(MsgBase):
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
lib_config = self.__run_config.get("library", None)
|
||||
if not lib_config:
|
||||
self._showError("未配置图书馆参数 !")
|
||||
return False
|
||||
url = lib_config.get("host_url") + lib_config.get("login_url")
|
||||
self.__driver.get(url)
|
||||
if not self.__waitResponseLoad():
|
||||
return False
|
||||
return True
|
||||
@@ -154,24 +175,26 @@ class AutoLib(MsgBase):
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
reserve_info: dict,
|
||||
login_config: dict,
|
||||
run_mode_config: dict,
|
||||
reserve_info: dict
|
||||
) -> int:
|
||||
|
||||
# result : 0 - success, 1 - failed, 2 - passed
|
||||
result = 1
|
||||
# result : -1 - terminate, 0 - success, 1 - failed, 2 - passed
|
||||
result = 2
|
||||
|
||||
# login
|
||||
if not self.__lib_login.login(
|
||||
username,
|
||||
password,
|
||||
self.__system_config_reader.get("login/max_attempt", 5),
|
||||
self.__system_config_reader.get("login/auto_captcha", True),
|
||||
login_config.get("max_attempt", 3),
|
||||
login_config.get("auto_captcha", True),
|
||||
):
|
||||
return 1
|
||||
"""
|
||||
Here, we collect the run mode from the config file.
|
||||
Here, we collect the run mode from the run config.
|
||||
"""
|
||||
run_mode = self.__system_config_reader.get("mode/run_mode", 0)
|
||||
run_mode = run_mode_config.get("run_mode", 0)
|
||||
run_mode = {
|
||||
"auto_reserve": run_mode&0x1,
|
||||
"auto_checkin": run_mode&0x2,
|
||||
@@ -180,57 +203,79 @@ class AutoLib(MsgBase):
|
||||
# reserve
|
||||
if run_mode["auto_reserve"]:
|
||||
if self.__lib_checker.canReserve(reserve_info.get("date")):
|
||||
if self.__lib_reserve.reserve(reserve_info):
|
||||
self._showTrace(f"用户 {username} 预约成功 !")
|
||||
if self.__lib_reserve.reserve(username, reserve_info):
|
||||
result = 0
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法预约,已跳过")
|
||||
result = 2
|
||||
# checkin
|
||||
if run_mode["auto_checkin"] and result == 2:
|
||||
if self.__lib_checker.canCheckin():
|
||||
if self.__lib_checkin.checkin(username):
|
||||
result = 0
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法签到,已跳过")
|
||||
result = 2
|
||||
# renewal
|
||||
if run_mode["auto_renewal"] and result == 2:
|
||||
if record := self.__lib_checker.canRenew():
|
||||
if self.__lib_renew.renew(username, record, reserve_info):
|
||||
if self.__lib_checker.postRenewCheck(record):
|
||||
result = 0
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法续约,已跳过")
|
||||
result = 2
|
||||
# logout
|
||||
if not self.__lib_logout.logout(
|
||||
username,
|
||||
username
|
||||
):
|
||||
# if logout is failed, we must make sure the host to be reloaded
|
||||
# otherwise, the next login may fail
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
return 1
|
||||
if not self.__initDriverUrl():
|
||||
return -1
|
||||
return result
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
system_config_reader: ConfigReader,
|
||||
users_config_reader: ConfigReader,
|
||||
user_config: dict
|
||||
):
|
||||
|
||||
self.__system_config_reader = system_config_reader
|
||||
self.__users_config_reader = users_config_reader
|
||||
if not self.__initBrowserDriver():
|
||||
return
|
||||
else:
|
||||
if not self.__initDriverUrl():
|
||||
return
|
||||
self.__initLibOperators()
|
||||
self.__user_config = user_config
|
||||
|
||||
user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0}
|
||||
users = self.__users_config_reader.get("users")
|
||||
self._showTrace(
|
||||
f"共发现 {len(users)} 个用户, "\
|
||||
f"用户配置文件路径: {self.__users_config_reader.configPath()}"
|
||||
)
|
||||
users = self.__user_config["users"]
|
||||
self._showTrace(f"共发现 {len(users)} 个用户")
|
||||
for user in users:
|
||||
user_counter["current"] += 1
|
||||
self._showTrace(
|
||||
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......"
|
||||
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......"
|
||||
)
|
||||
if not user["enabled"]:
|
||||
self._showTrace(f"用户 {user["username"]} 已跳过")
|
||||
user_counter["passed"] += 1
|
||||
continue
|
||||
r = self.__run(
|
||||
username=user["username"],
|
||||
password=user["password"],
|
||||
login_config=self.__run_config["login"],
|
||||
run_mode_config=self.__run_config["mode"],
|
||||
reserve_info=user["reserve_info"],
|
||||
)
|
||||
if r == 0:
|
||||
if r == -1:
|
||||
self._showTrace(
|
||||
f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !"
|
||||
)
|
||||
break
|
||||
elif r == 0:
|
||||
user_counter["success"] += 1
|
||||
elif r == 1:
|
||||
user_counter["failed"] += 1
|
||||
@@ -245,7 +290,7 @@ class AutoLib(MsgBase):
|
||||
|
||||
|
||||
def close(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
if self.__driver:
|
||||
@@ -0,0 +1,374 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import queue
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibChecker(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def __formatDiffTime(
|
||||
seconds: float
|
||||
) -> str:
|
||||
|
||||
hours = int(seconds//3600)
|
||||
minutes = int(seconds%3600//60)
|
||||
seconds = int(seconds%60)
|
||||
return f"{hours} 时 {minutes} 分 {seconds} 秒"
|
||||
|
||||
|
||||
def __navigateToReserveRecordPage(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __decodeReserveTime(
|
||||
self,
|
||||
time_element
|
||||
) -> dict:
|
||||
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
|
||||
if date_match:
|
||||
date = date_match.group(1)
|
||||
else:
|
||||
date = ""
|
||||
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
|
||||
if time_match:
|
||||
begin_time = time_match.group(1)
|
||||
end_time = time_match.group(2)
|
||||
else:
|
||||
begin_time = ""
|
||||
end_time = ""
|
||||
return {
|
||||
"date": date,
|
||||
"time": {
|
||||
"begin": begin_time,
|
||||
"end": end_time
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveInfo(
|
||||
self,
|
||||
info_elements
|
||||
) -> str:
|
||||
|
||||
location = ""
|
||||
status = ""
|
||||
for info in info_elements:
|
||||
if "已预约" in info.text:
|
||||
status = "已预约"
|
||||
elif "使用中" in info.text:
|
||||
status = "使用中"
|
||||
elif "已完成" in info.text:
|
||||
status = "已完成"
|
||||
elif "已结束使用" in info.text:
|
||||
status = "已结束使用"
|
||||
elif "已取消" in info.text:
|
||||
status = "已取消"
|
||||
elif "失约" in info.text:
|
||||
status = "失约"
|
||||
elif "图书馆" in info.text:
|
||||
location = info.text.strip()
|
||||
return {
|
||||
"location": location,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveRecord(
|
||||
self,
|
||||
reservation
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
info_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
except:
|
||||
return {
|
||||
"date": "",
|
||||
"time": {"begin": "", "end": ""},
|
||||
"info": {"location": "", "status": ""}
|
||||
}
|
||||
time = self.__decodeReserveTime(time_element)
|
||||
info = self.__decodeReserveInfo(info_elements)
|
||||
return {
|
||||
"date": time["date"],
|
||||
"time": time["time"],
|
||||
"info": info
|
||||
}
|
||||
|
||||
|
||||
def __loadReserveRecords(
|
||||
self
|
||||
) -> list:
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
|
||||
)
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
|
||||
)
|
||||
return reservations
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return None
|
||||
|
||||
|
||||
def __showMoreReserveRecords(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
WebDriverWait(self.__driver, 0.1).until(
|
||||
EC.element_to_be_clickable((By.ID, "moreBtn"))
|
||||
)
|
||||
except:
|
||||
# the reservation is the last one
|
||||
return False
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
return True
|
||||
else:
|
||||
self._showTrace("用户无法加载更多预约记录")
|
||||
return False
|
||||
except:
|
||||
self._showTrace("加载更多预约记录失败 !")
|
||||
return False
|
||||
|
||||
|
||||
def __getReserveRecord(
|
||||
self,
|
||||
wanted_date: str,
|
||||
wanted_status: str
|
||||
) -> dict:
|
||||
|
||||
if wanted_date is None:
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态")
|
||||
return None
|
||||
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
|
||||
|
||||
checked_count = 0
|
||||
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
|
||||
|
||||
if not self.__navigateToReserveRecordPage():
|
||||
return None
|
||||
for _ in range(max_check_times):
|
||||
reservations = self.__loadReserveRecords()
|
||||
if reservations is None:
|
||||
return None
|
||||
for reservation in reservations[checked_count:]:
|
||||
record = self.__decodeReserveRecord(reservation)
|
||||
checked_count += 1
|
||||
if record is None:
|
||||
continue
|
||||
if record["date"] == "":
|
||||
continue
|
||||
if record["time"] == {"begin": "", "end": ""}:
|
||||
continue
|
||||
# record date is later than the given date, check the next one
|
||||
if datetime.strptime(record["date"], "%Y-%m-%d").date() >\
|
||||
datetime.strptime(wanted_date, "%Y-%m-%d").date():
|
||||
continue
|
||||
# record date is earlier than the given date, so there is no wanted record
|
||||
if datetime.strptime(record["date"], "%Y-%m-%d").date() <\
|
||||
datetime.strptime(wanted_date, "%Y-%m-%d").date():
|
||||
return None
|
||||
if record["info"]["status"] == wanted_status:
|
||||
self._showTrace(
|
||||
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
|
||||
f"详细信息: {record["date"]} "
|
||||
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
|
||||
)
|
||||
return record
|
||||
if not self.__showMoreReserveRecords():
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# no reserved or using record in the given date
|
||||
# then can reserve
|
||||
if self.__getReserveRecord(date, "已预约") is None:
|
||||
if self.__getReserveRecord(date, "使用中") is None:
|
||||
self._showTrace(f"用户在 {date} 可以预约")
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约")
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
|
||||
|
||||
def canCheckin(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# only check the current date
|
||||
date = time.strftime("%Y-%m-%d", time.localtime())
|
||||
record = self.__getReserveRecord(date, "已预约")
|
||||
if record is not None:
|
||||
begin_time = record["time"]["begin"]
|
||||
begin_time = datetime.strptime(f"{date} {begin_time}", "%Y-%m-%d %H:%M")
|
||||
time_diff = datetime.now() - begin_time
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# before 30 minutes, cant checkin
|
||||
if time_diff_seconds < -30*60:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
|
||||
)
|
||||
return False
|
||||
# before in 30 minutes, can checkin
|
||||
elif -30*60 <= time_diff_seconds < 0:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
# past less than 30 minutes, can checkin
|
||||
elif 0 <= time_diff_seconds < 30*60 - 5: # spare 5 seconds for the checkin process
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前距离预约开始时间已经过去 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
|
||||
return False
|
||||
|
||||
|
||||
def canRenew(
|
||||
self
|
||||
):
|
||||
|
||||
# only check the current date
|
||||
date = time.strftime("%Y-%m-%d", time.localtime())
|
||||
record = self.__getReserveRecord(date, "使用中")
|
||||
if record is not None:
|
||||
end_time = record["time"]["end"]
|
||||
end_time = datetime.strptime(f"{date} {end_time}", "%Y-%m-%d %H:%M")
|
||||
time_diff = end_time - datetime.now()
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# a using record is definitely after the begin time
|
||||
trace_msg = (
|
||||
f"用户在 {date} 的预约结束时间为 {end_time}, "
|
||||
f"当前距离预约结束时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}"
|
||||
)
|
||||
if abs(time_diff_seconds) < 120*60:
|
||||
self._showTrace(f"{trace_msg}, 可以续约")
|
||||
return record
|
||||
else:
|
||||
self._showTrace(f"{trace_msg}, 无法续约")
|
||||
return None
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
|
||||
return None
|
||||
|
||||
|
||||
def postRenewCheck(
|
||||
self,
|
||||
record: dict
|
||||
):
|
||||
"""
|
||||
Check if the renew operation is successful
|
||||
|
||||
Args:
|
||||
record (dict): The expected record after renewal
|
||||
|
||||
Returns:
|
||||
bool: True if the renew operation is successful, False otherwise
|
||||
"""
|
||||
# because the special circumstance that the renew operation
|
||||
# do not show the success message or anything else,
|
||||
# we need to check the record data to make sure the renew operation is successful.
|
||||
|
||||
# only check the given record date
|
||||
date = record["date"]
|
||||
act_record = self.__getReserveRecord(date, "使用中")
|
||||
if act_record is not None:
|
||||
if act_record["time"]["begin"] == record["time"]["begin"] and\
|
||||
act_record["time"]["end"] == record["time"]["end"]:
|
||||
self._showTrace(f"\n"\
|
||||
f" 续约成功 !\n"\
|
||||
f" 日 期 :{date}\n"\
|
||||
f" 时 间 :{act_record["time"]["begin"]} - {act_record["time"]["end"]}\n"\
|
||||
f" 位 置 :{act_record["info"]["location"]}\n"
|
||||
f" 状 态 :{act_record["info"]["status"]}"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"\n"\
|
||||
f" 续约失败 !\n"\
|
||||
f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
|
||||
)
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果")
|
||||
return False
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import queue
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibCheckin(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "resultMessage"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.CLASS_NAME, "btnOK"))
|
||||
)
|
||||
result_message_element = self.__driver.find_element(
|
||||
By.CLASS_NAME, "resultMessage"
|
||||
)
|
||||
ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK")
|
||||
except:
|
||||
self._showTrace("签到时发生未知错误 !")
|
||||
return False
|
||||
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(f"\n"\
|
||||
" 签到成功 !\n"\
|
||||
" 未获取到签到详情 !"
|
||||
)
|
||||
ok_btn.click()
|
||||
return True
|
||||
else:
|
||||
failure_reason = result_message.replace("签到失败", "").strip()
|
||||
self._showTrace(f"\n"\
|
||||
" 签到失败 !\n"\
|
||||
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()
|
||||
if self._waitResponseLoad():
|
||||
self._showTrace(f"用户 {username} 签到成功 !")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 签到失败 !")
|
||||
return False
|
||||
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibCheckout(LibOperator):
|
||||
@@ -25,7 +25,7 @@ class LibCheckout(LibOperator):
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
@@ -34,7 +34,7 @@ class LibCheckout(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -17,7 +17,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibLogin(LibOperator):
|
||||
@@ -26,7 +26,7 @@ class LibLogin(LibOperator):
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
@@ -36,30 +36,30 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# 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
|
||||
except Exception as e:
|
||||
self._showTrace(f"登录页面加载失败 ! : {e}")
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准")
|
||||
return False
|
||||
|
||||
|
||||
def __fillLogInElements(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
password: str
|
||||
) -> bool:
|
||||
|
||||
# ensure elements presence and fill them
|
||||
@@ -77,7 +77,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def __autoRecognizeCaptcha(
|
||||
self,
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# auto recognize captcha
|
||||
@@ -99,7 +99,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def __manualRecognizeCaptcha(
|
||||
self,
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# manual recognize captcha
|
||||
@@ -117,7 +117,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def __refreshCaptcha(
|
||||
self,
|
||||
self
|
||||
):
|
||||
|
||||
# refresh captcha
|
||||
@@ -136,7 +136,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
def __solveCaptcha(
|
||||
self,
|
||||
auto_captcha: bool = True,
|
||||
auto_captcha: bool = True
|
||||
) -> str:
|
||||
|
||||
max_attempts = 5
|
||||
@@ -155,7 +155,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
def __fillCaptchaElement(
|
||||
self,
|
||||
captcha_text: str,
|
||||
captcha_text: str
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
@@ -174,7 +174,7 @@ class LibLogin(LibOperator):
|
||||
username: str,
|
||||
password: str,
|
||||
max_attempts: int = 5,
|
||||
auto_captcha: bool = True,
|
||||
auto_captcha: bool = True
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
@@ -13,7 +13,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibLogout(LibOperator):
|
||||
@@ -22,7 +22,7 @@ class LibLogout(LibOperator):
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver,
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
@@ -31,7 +31,7 @@ class LibLogout(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return True
|
||||
@@ -39,7 +39,7 @@ class LibLogout(LibOperator):
|
||||
|
||||
def logout(
|
||||
self,
|
||||
username: str,
|
||||
username: str
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
@@ -0,0 +1,216 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import queue
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibRenew(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
self.__driver.refresh()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def __timeToMins(
|
||||
time_str: str
|
||||
) -> int:
|
||||
|
||||
hour, minute = map(int, time_str.split(":"))
|
||||
return hour*60 + minute
|
||||
|
||||
@staticmethod
|
||||
def __minsToTime(
|
||||
mins: int
|
||||
) -> str:
|
||||
|
||||
hour, minute = divmod(mins, 60)
|
||||
return f"{hour:02d}:{minute:02d}"
|
||||
|
||||
|
||||
def __waitRenewDialog(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.visibility_of_element_located((By.ID, "extendDiv"))
|
||||
)
|
||||
head_message = WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv p.messageHead"))
|
||||
)
|
||||
result_message = WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("续约时间选择界面加载失败 !")
|
||||
return False
|
||||
head_message = head_message.text.strip()
|
||||
if "警告" in head_message:
|
||||
result_message = result_message.text.strip()
|
||||
self._showTrace(f"\n"\
|
||||
f" 续约失败 !\n"\
|
||||
f" {result_message}")
|
||||
return False
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_all_elements_located(
|
||||
(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
|
||||
)
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("续约时间选择界面加载失败 !")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __selectNearstTime(
|
||||
self,
|
||||
record: dict,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
"""
|
||||
TODO : this function is too long and too ugly
|
||||
|
||||
we need to refactor it to make it more readable.
|
||||
but may be it is not a good idea to refactor it. :) who knows...
|
||||
"""
|
||||
|
||||
end_time = record["time"]["end"]
|
||||
renew_info = reserve_info["renew_time"]
|
||||
max_diff = renew_info["max_diff"]
|
||||
prefer_earlier = renew_info["prefer_early"]
|
||||
target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60
|
||||
renew_ok_btn = self.__driver.find_element(
|
||||
By.CSS_SELECTOR, "#extendDiv .btnOK"
|
||||
)
|
||||
try:
|
||||
renew_time_opts = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, "#extendDiv .renewal_List li"
|
||||
)
|
||||
free_times = []
|
||||
best_time_diff = max_diff
|
||||
best_actual_diff = None
|
||||
best_time_opt = None
|
||||
|
||||
if not renew_time_opts:
|
||||
self._showTrace("当前未查询到可用续约时间 !")
|
||||
return False
|
||||
for time_opt in renew_time_opts:
|
||||
time_attr = time_opt.get_attribute("id")
|
||||
if time_attr and time_attr.isdigit():
|
||||
time_val = int(time_attr)
|
||||
free_times.append(time_opt.text.strip())
|
||||
else:
|
||||
continue
|
||||
actual_diff = time_val - target_renew_mins
|
||||
abs_diff = abs(actual_diff)
|
||||
if abs_diff < best_time_diff or (
|
||||
abs_diff == best_time_diff and (
|
||||
# 优先选择更早的时间
|
||||
(prefer_earlier and actual_diff <= 0) or
|
||||
# 优先选择更晚的时间
|
||||
(not prefer_earlier and actual_diff >= 0)
|
||||
)
|
||||
):
|
||||
best_time_diff = abs_diff
|
||||
best_actual_diff = actual_diff
|
||||
best_time_opt = time_opt
|
||||
|
||||
if best_time_opt is not None:
|
||||
best_time_opt.click()
|
||||
abs_time_diff = abs(best_actual_diff)
|
||||
if best_actual_diff < 0:
|
||||
time_relation = f"早了 {abs_time_diff} 分钟"
|
||||
elif best_actual_diff > 0:
|
||||
time_relation = f"晚了 {abs_time_diff} 分钟"
|
||||
else:
|
||||
time_relation = f"正好等于续约时间"
|
||||
self._showTrace(
|
||||
f"选择距离期望续约时间最近的 {best_time_opt.text}, "\
|
||||
f"与期望续约时间相比 {time_relation}"
|
||||
)
|
||||
# update the actual renew end time
|
||||
record["time"]["end"] = best_time_opt.text.strip()
|
||||
renew_ok_btn.click()
|
||||
return True
|
||||
self._showTrace(
|
||||
"无法选择最近的可用续约时间 !" \
|
||||
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
|
||||
)
|
||||
self._showTrace(
|
||||
f"当前可供续约的时间有: {free_times}"
|
||||
)
|
||||
return False
|
||||
except:
|
||||
self._showTrace("查询可用续约时间时发生未知错误 !")
|
||||
return False
|
||||
|
||||
|
||||
def renew(
|
||||
self,
|
||||
username: str,
|
||||
record: dict,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
return False
|
||||
try:
|
||||
renew_btn = WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "btnExtend"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"用户 {username} 续约界面加载失败 !")
|
||||
return False
|
||||
if "disabled" in renew_btn.get_attribute("class"):
|
||||
self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
|
||||
return False
|
||||
renew_btn.click()
|
||||
if not self.__waitRenewDialog():
|
||||
self._showTrace(f"用户 {username} 续约失败 !")
|
||||
|
||||
# After the renewal, the webpage will display a mask overlay,
|
||||
# so we need to refresh the page for subsequent operations.
|
||||
self.__driver.refresh()
|
||||
return False
|
||||
if not self.__selectNearstTime(record, reserve_info):
|
||||
self._showTrace(f"用户 {username} 续约失败 !")
|
||||
self.__driver.refresh()
|
||||
return False
|
||||
if self._waitResponseLoad():
|
||||
return True
|
||||
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibReserve(LibOperator):
|
||||
@@ -25,7 +25,7 @@ class LibReserve(LibOperator):
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
@@ -55,13 +55,13 @@ class LibReserve(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "layoutSeat"))
|
||||
)
|
||||
title_elements = []
|
||||
# reserve failed without title elements, so we need to try
|
||||
try:
|
||||
WebDriverWait(self.__driver, 1).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".layoutSeat dt"))
|
||||
)
|
||||
title_elements = self.__driver.find_elements(
|
||||
@@ -83,20 +83,18 @@ 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"\
|
||||
f" 未找获取到详细信息")
|
||||
self._showTrace("\n"\
|
||||
" 预约成功 !\n"\
|
||||
" 未找获取到详细信息"
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"预约结果加载失败 !")
|
||||
@@ -119,26 +117,26 @@ class LibReserve(LibOperator):
|
||||
return f"{hour:02d}:{minute:02d}"
|
||||
|
||||
|
||||
def __checkReserveInfo(
|
||||
def __containRequiredInfo(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
# check the required information
|
||||
# reserve_info["place"]
|
||||
if reserve_info.get("floor") is None:
|
||||
# must contain the required infomation
|
||||
if reserve_info.get("floor") is None: # if existence ?
|
||||
raise ValueError("未指定楼层")
|
||||
if reserve_info["floor"] not in self.__floor_map:
|
||||
raise ValueError(f"楼层 '{reserve_info['floor']}' 不存在")
|
||||
if reserve_info["floor"] not in self.__floor_map: # if in the mao ?
|
||||
raise ValueError(f"该楼层 '{reserve_info['floor']}' 不存在")
|
||||
if reserve_info.get("room") is None:
|
||||
raise ValueError("未指定房间")
|
||||
if reserve_info["room"] not in self.__room_map:
|
||||
raise ValueError(f"房间 '{reserve_info['room']}' 不存在")
|
||||
raise ValueError(f"该房间 '{reserve_info['room']}' 不存在")
|
||||
if reserve_info.get("seat_id") is None:
|
||||
raise ValueError("未指定座位")
|
||||
if reserve_info["seat_id"] == "":
|
||||
raise ValueError("未指定座位号")
|
||||
return True
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"预约信息错误 ! : {e}, "\
|
||||
@@ -146,10 +144,14 @@ class LibReserve(LibOperator):
|
||||
)
|
||||
return False
|
||||
|
||||
# check and try to fix the time errors
|
||||
cur_time_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
||||
cur_date, curr_time = cur_time_str.split()
|
||||
if not reserve_info.get("date"):
|
||||
|
||||
def __isValidDate(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
cur_date = time.strftime("%Y-%m-%d", time.localtime())
|
||||
if reserve_info.get("date") is None:
|
||||
reserve_info["date"] = cur_date
|
||||
self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date}")
|
||||
else:
|
||||
@@ -159,94 +161,133 @@ class LibReserve(LibOperator):
|
||||
f"{reserve_info['date']} 早于当前日期 {cur_date}, 自动设置为当前日期"
|
||||
)
|
||||
reserve_info["date"] = cur_date
|
||||
# check the begin time
|
||||
begin_time = reserve_info.get("begin_time")
|
||||
if not begin_time:
|
||||
reserve_info["begin_time"] = {
|
||||
"time": curr_time,
|
||||
"max_diff": 30,
|
||||
"prefer_early": True
|
||||
}
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}, 最大时间差为 30 分钟, 优先选择更早预约时间")
|
||||
else:
|
||||
begin_time = reserve_info["begin_time"]
|
||||
if "time" not in begin_time:
|
||||
begin_time["time"] = curr_time
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}")
|
||||
if "max_diff" not in begin_time:
|
||||
begin_time["max_diff"] = 30
|
||||
self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in begin_time:
|
||||
begin_time["prefer_early"] = True
|
||||
self._showTrace(f"是否优先选择更早预约时间未指定, 自动设置为 True")
|
||||
expect_duration = reserve_info.get("expect_duration")
|
||||
if not expect_duration:
|
||||
reserve_info["expect_duration"] = 4
|
||||
expect_duration = 4
|
||||
self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
|
||||
if not reserve_info.get("satisfy_duration"):
|
||||
return True
|
||||
|
||||
|
||||
def __isValidBeginTime(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
cur_time = time.strftime("%H:%M", time.localtime())
|
||||
if reserve_info.get("begin_time") is None:
|
||||
reserve_info["begin_time"] = {}
|
||||
if "time" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["time"] = cur_time
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {cur_time}")
|
||||
if "max_diff" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["max_diff"] = 30
|
||||
self._showTrace(f"开始时间最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["prefer_early"] = True
|
||||
self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True")
|
||||
return True
|
||||
|
||||
|
||||
def __isValidExpectDuration(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
if reserve_info.get("satisfy_duration") is None:
|
||||
reserve_info["satisfy_duration"] = True
|
||||
self._showTrace("预约满足时长要求未指定, 默认满足")
|
||||
# check the end time
|
||||
if not reserve_info.get("end_time"):
|
||||
begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = begin_mins + reserve_info["expect_duration"] * 60
|
||||
end_time_str = self.__minsToTime(end_mins)
|
||||
if reserve_info["satisfy_duration"]:
|
||||
if reserve_info.get("expect_duration") is None:
|
||||
reserve_info["expect_duration"] = 4
|
||||
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
|
||||
return True
|
||||
|
||||
|
||||
def __isValidEndTime(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
if reserve_info.get("end_time") is None:
|
||||
reserve_info["end_time"] = {}
|
||||
if "time" not in reserve_info["end_time"]:
|
||||
end_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = end_mins + int(reserve_info["expect_duration"]*60)
|
||||
reserve_info["end_time"] = {
|
||||
"time": end_time_str,
|
||||
"time": self.__minsToTime(end_mins),
|
||||
"max_diff": 30,
|
||||
"prefer_early": False
|
||||
}
|
||||
self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time_str}, 最大时间差为 30 分钟, 优先选择较晚预约时间")
|
||||
self._showTrace(
|
||||
f"结束时间未指定, 自动设置为开始时间加上期望时长: {reserve_info['end_time']['time']}"
|
||||
)
|
||||
if "max_diff" not in reserve_info["end_time"]:
|
||||
reserve_info["end_time"]["max_diff"] = 30
|
||||
self._showTrace(f"结束时间最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in reserve_info["end_time"]:
|
||||
reserve_info["end_time"]["prefer_early"] = False
|
||||
self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True")
|
||||
return True
|
||||
|
||||
|
||||
def __finalCheck(
|
||||
self,
|
||||
reserve_info: dict
|
||||
):
|
||||
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self.__timeToMins(begin_time["time"])
|
||||
end_mins = self.__timeToMins(end_time["time"])
|
||||
# if end time is earlier than begin_time, exchange them
|
||||
if end_mins < begin_mins:
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
|
||||
)
|
||||
reserve_info["end_time"] = begin_time
|
||||
reserve_info["begin_time"] = end_time
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self.__timeToMins(begin_time["time"])
|
||||
end_mins = self.__timeToMins(end_time["time"])
|
||||
# ensure the end time is not later than 23:30
|
||||
if end_mins > self.__timeToMins("23:30"):
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30"
|
||||
)
|
||||
reserve_info["end_time"]["time"] = "23:30"
|
||||
end_mins = self.__timeToMins("23:30")
|
||||
# ensure the duration is not longer than 8 hours
|
||||
if reserve_info["satisfy_duration"]:
|
||||
if reserve_info["expect_duration"] > 8:
|
||||
self._showTrace(
|
||||
f"该用户设置了优先满足时长要求, 但是预约期望持续时间 "
|
||||
f"{reserve_info['expect_duration']} 小时 "
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时"
|
||||
)
|
||||
reserve_info["expect_duration"] = 8
|
||||
else:
|
||||
end_time = reserve_info["end_time"]
|
||||
if "time" not in end_time:
|
||||
begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = begin_mins + reserve_info["expect_duration"] * 60
|
||||
end_time["time"] = self.__minsToTime(end_mins)
|
||||
self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time['time']}")
|
||||
if "max_diff" not in end_time:
|
||||
end_time["max_diff"] = 30
|
||||
self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in end_time:
|
||||
end_time["prefer_early"] = False
|
||||
self._showTrace(f"是否优先选择较早预约时间未指定, 自动设置为 False")
|
||||
# check the reserve time boundary and fix the errors
|
||||
#
|
||||
# get time string for message show
|
||||
begin_time_str = reserve_info["begin_time"]["time"]
|
||||
end_time_str = reserve_info["end_time"]["time"]
|
||||
if end_mins - begin_mins > 8*60:
|
||||
self._showTrace(
|
||||
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
|
||||
f"{float((end_mins - begin_mins)/60)} 小时 "
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时"
|
||||
)
|
||||
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60)
|
||||
return True
|
||||
|
||||
# minute time for check and fix them
|
||||
begin_mins = self.__timeToMins(begin_time_str)
|
||||
end_mins = self.__timeToMins(end_time_str)
|
||||
|
||||
# ensure begin time is not later than end time
|
||||
if begin_mins > end_mins:
|
||||
reserve_info["begin_time"]["time"], reserve_info["end_time"]["time"] = end_time_str, begin_time_str
|
||||
reserve_info["begin_time"]["prefer_early"], reserve_info["end_time"]["prefer_early"] = \
|
||||
reserve_info["end_time"]["prefer_early"], reserve_info["begin_time"]["prefer_early"]
|
||||
self._showTrace("预约开始时间晚于预约结束时间, 自动调换开始时间和结束时间")
|
||||
def __checkReserveInfo(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
# update the begin_mins and end_mins after swap
|
||||
begin_time_str, end_time_str = end_time_str, begin_time_str
|
||||
begin_mins, end_mins = end_mins, begin_mins
|
||||
|
||||
# ensure end time is not later than 22:30
|
||||
max_end_mins = self.__timeToMins("22:30")
|
||||
if end_mins > max_end_mins:
|
||||
reserve_info["end_time"]["time"] = "22:30"
|
||||
end_time_str = "22:30"
|
||||
end_mins = max_end_mins
|
||||
self._showTrace("预约结束时间超过 22:30, 自动设置为 22:30")
|
||||
|
||||
# ensure expect duration is shorter than 8 hours
|
||||
max_duration_mins = 8 * 60
|
||||
duration_mins = end_mins - begin_mins
|
||||
if duration_mins > max_duration_mins:
|
||||
new_end_mins = begin_mins + max_duration_mins
|
||||
reserve_info["end_time"]["time"] = self.__minsToTime(new_end_mins)
|
||||
self._showTrace("预约持续时间超过8小时, 自动设置为 8 小时")
|
||||
if not self.__containRequiredInfo(reserve_info):
|
||||
return False
|
||||
if not self.__isValidDate(reserve_info):
|
||||
return False
|
||||
if not self.__isValidBeginTime(reserve_info):
|
||||
return False
|
||||
if not self.__isValidExpectDuration(reserve_info):
|
||||
return False
|
||||
if not self.__isValidEndTime(reserve_info):
|
||||
return False
|
||||
if not self.__finalCheck(reserve_info):
|
||||
return False
|
||||
self._showTrace(
|
||||
f"预约信息检查完成, 准备预约 "
|
||||
f"{reserve_info['date']} "
|
||||
@@ -265,17 +306,17 @@ class LibReserve(LibOperator):
|
||||
trigger_locator: tuple,
|
||||
fail_msg: str,
|
||||
success_msg: str,
|
||||
option_locator: tuple = None,
|
||||
option_locator: tuple = None
|
||||
) -> bool:
|
||||
|
||||
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)
|
||||
@@ -285,11 +326,52 @@ class LibReserve(LibOperator):
|
||||
return False
|
||||
|
||||
|
||||
def __clickElementByJS(
|
||||
self,
|
||||
trigger_locator_id: str,
|
||||
option_query_selector: str,
|
||||
fail_msg: str,
|
||||
success_msg: str,
|
||||
) -> bool:
|
||||
|
||||
script = f"""
|
||||
try {{
|
||||
var trigger = document.getElementById('{trigger_locator_id}');
|
||||
if (trigger) {{
|
||||
trigger.click();
|
||||
var option = document.querySelector("{option_query_selector}");
|
||||
if (option) {{
|
||||
option.click();
|
||||
return true;
|
||||
}}
|
||||
return false;
|
||||
}}
|
||||
return false;
|
||||
}} catch (e) {{
|
||||
return false;
|
||||
}}
|
||||
"""
|
||||
result = self.__driver.execute_script(script)
|
||||
time.sleep(0.1)
|
||||
if result:
|
||||
self._showTrace(success_msg)
|
||||
else:
|
||||
self._showTrace(fail_msg)
|
||||
return result
|
||||
|
||||
|
||||
def __selectDate(
|
||||
self,
|
||||
date_str: str
|
||||
) -> bool:
|
||||
|
||||
if self.__clickElementByJS(
|
||||
trigger_locator_id="onDate_select",
|
||||
option_query_selector=f"p#options_onDate a[value='{date_str}']",
|
||||
success_msg=f"日期 {date_str} 选择成功 !",
|
||||
fail_msg=f"选择日期失败 ! : {date_str} 不可用"
|
||||
):
|
||||
return True
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "onDate_select"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"),
|
||||
@@ -303,12 +385,20 @@ class LibReserve(LibOperator):
|
||||
place: str
|
||||
) -> bool:
|
||||
|
||||
actual_place = "1" if place == "图书馆" else "1"
|
||||
place = "1" # the library only have this place :)
|
||||
display_place = "图书馆"
|
||||
if self.__clickElementByJS(
|
||||
trigger_locator_id="display_building",
|
||||
option_query_selector=f"p#options_building a[value='{place}']",
|
||||
success_msg=f"预约场所 {display_place} 选择成功 !",
|
||||
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
|
||||
):
|
||||
return True
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "display_building"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{actual_place}']"),
|
||||
success_msg=f"预约场所 {place} 选择成功 !",
|
||||
fail_msg=f"选择预约场所失败 ! : {place} 不可用"
|
||||
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{place}']"),
|
||||
success_msg=f"预约场所 {display_place} 选择成功 !",
|
||||
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
|
||||
)
|
||||
|
||||
|
||||
@@ -318,6 +408,13 @@ class LibReserve(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
display_floor = self.__floor_map.get(floor)
|
||||
if self.__clickElementByJS(
|
||||
trigger_locator_id="floor_select",
|
||||
option_query_selector=f"p#options_floor a[value='{floor}']",
|
||||
success_msg=f"楼层 {display_floor} 选择成功 !",
|
||||
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
|
||||
):
|
||||
return True
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "floor_select"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"),
|
||||
@@ -332,12 +429,24 @@ class LibReserve(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
display_room = self.__room_map.get(room)
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, f"room_{room}"),
|
||||
option_locator=None,
|
||||
success_msg=f"房间 {display_room} 选择成功 !",
|
||||
fail_msg=f"选择房间失败 ! : {display_room} 不可用"
|
||||
)
|
||||
# find room
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "findRoom"))
|
||||
).click()
|
||||
except:
|
||||
self._showTrace("加载房间/区域失败 !")
|
||||
return False
|
||||
# select room
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, f"room_{room}"))
|
||||
).click()
|
||||
self._showTrace(f"房间 {display_room} 选择成功 !")
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"选择房间失败 ! : {display_room} 不可用")
|
||||
return False
|
||||
|
||||
|
||||
def __selectSeat(
|
||||
@@ -347,9 +456,16 @@ class LibReserve(LibOperator):
|
||||
|
||||
try:
|
||||
# wait fot seat layout element to load
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"座位加载失败 !")
|
||||
return False
|
||||
try:
|
||||
all_seats = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, "li[id^='seat_']"
|
||||
)
|
||||
@@ -358,7 +474,7 @@ class LibReserve(LibOperator):
|
||||
if not seat_id_upper == seat.text.lstrip('0'):
|
||||
continue
|
||||
seat_link = seat.find_element(By.TAG_NAME, "a")
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(seat_link)
|
||||
)
|
||||
seat_link.click()
|
||||
@@ -380,6 +496,15 @@ class LibReserve(LibOperator):
|
||||
prefer_earlier: bool = True
|
||||
) -> int:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_all_elements_located(
|
||||
(By.CSS_SELECTOR, f"#{time_id} ul li a")
|
||||
)
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
return -1
|
||||
try:
|
||||
all_time_opts = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR,
|
||||
@@ -390,6 +515,9 @@ class LibReserve(LibOperator):
|
||||
best_actual_diff = None
|
||||
best_time_opt = None
|
||||
|
||||
if not all_time_opts:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
return -1
|
||||
for time_opt in all_time_opts:
|
||||
time_attr = time_opt.get_attribute("time")
|
||||
if time_attr == "now":
|
||||
@@ -451,6 +579,7 @@ class LibReserve(LibOperator):
|
||||
expect_begin_time = actual_begin_time = begin_time["time"]
|
||||
expect_end_time = actual_end_time = end_time["time"]
|
||||
expect_begin_mins = self.__timeToMins(expect_begin_time)
|
||||
actual_begin_mins = expect_begin_mins
|
||||
expect_end_mins = self.__timeToMins(expect_end_time)
|
||||
|
||||
# select the begin time
|
||||
@@ -464,11 +593,18 @@ class LibReserve(LibOperator):
|
||||
return False
|
||||
else:
|
||||
actual_begin_time = self.__minsToTime(expect_begin_mins)
|
||||
actual_begin_mins = self.__timeToMins(actual_begin_time)
|
||||
# if 'satisfy_duration' is True.
|
||||
# select the end time based on the begin time
|
||||
# (because it may be changed under the 'max time diff' strategy) and expect duration.
|
||||
if satisfy_duration:
|
||||
expect_end_mins = int(expect_begin_mins + expct_duration*60)
|
||||
expect_end_mins = int(actual_begin_mins + expct_duration*60)
|
||||
if expect_end_mins > self.__timeToMins("23:30"):
|
||||
expect_end_mins = self.__timeToMins("23:30")
|
||||
self._showTrace(
|
||||
f"预约持续时间 {expct_duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
|
||||
)
|
||||
expect_end_time = self.__minsToTime(expect_end_mins)
|
||||
self._showTrace(
|
||||
f"需要满足期望预约持续时间: {expct_duration} 小时, "\
|
||||
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
|
||||
@@ -493,6 +629,7 @@ class LibReserve(LibOperator):
|
||||
|
||||
def reserve(
|
||||
self,
|
||||
username: str,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
@@ -505,7 +642,7 @@ class LibReserve(LibOperator):
|
||||
return False
|
||||
# map page
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
@@ -514,22 +651,13 @@ class LibReserve(LibOperator):
|
||||
except:
|
||||
self._showTrace(f"加载预约选座页面失败 !")
|
||||
return False
|
||||
# date, place, floor
|
||||
# date, place, floor, room
|
||||
if not self.__selectDate(reserve_info["date"]):
|
||||
return False
|
||||
if not self.__selectPlace(reserve_info["place"]):
|
||||
return False
|
||||
if not self.__selectFloor(reserve_info["floor"]):
|
||||
return False
|
||||
# room find
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "findRoom"))
|
||||
).click()
|
||||
except:
|
||||
self._showTrace("加载房间/区域失败 !")
|
||||
return False
|
||||
# room
|
||||
if not self.__selectRoom(reserve_info["room"]):
|
||||
return False
|
||||
else:
|
||||
@@ -557,4 +685,8 @@ class LibReserve(LibOperator):
|
||||
self._showTrace(f"预约提交失败 !")
|
||||
if not submit_reserve and have_hover_on_page:
|
||||
self.__driver.refresh()
|
||||
if reserve_success:
|
||||
self._showTrace(f"用户 {username} 预约成功 !")
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
return reserve_success
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Operators module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- AutoLib: AutoLibrary operator.
|
||||
- LibLogin: Library operator for logging in.
|
||||
- LibLogout: Library operator for logging out.
|
||||
- LibReserve: Library operator for reserving seat.
|
||||
- LibCheckin: Library operator for checking in seat.
|
||||
- LibCheckout: Library operator for checking out seat.
|
||||
- LibRenew: Library operator for renewing seat.
|
||||
"""
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Utils module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- ConfigReader: Configuration reader class for the AutoLibrary project.
|
||||
- ConfigWriter: Configuration writer class for the AutoLibrary project.
|
||||
"""
|
||||