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

Compare commits

...

23 Commits

Author SHA1 Message Date
KenanZhu 6d05d4c7cb ci: main 分支 push 事件触发 BuildTest 工作流 2026-06-17 10:29:54 +08:00
KenanZhu 01f4ccaa0e fix(driver): macOS/Linux 下载的 Chrome/Edge 驱动缺少执行权限导致运行失败
zipfile 解压不保留 Unix 执行位,extract 后 chmod 755;同时在启动检查
时对已下载但不可执行的旧驱动自动修复权限。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 10:22:33 +08:00
Kenan Zhu 0bad34d7a8 refactor(*): LoginPage 消息追踪统一与 Flow 长方法拆分 (#12)
refactor: LoginPage 消息追踪统一与 Flow 长方法拆分
2026-06-13 09:30:02 +08:00
KenanZhu 61d1b44402 refactor: 提取 Flow 类 execute 长方法为私有子方法,降低嵌套深度 2026-06-13 08:59:45 +08:00
KenanZhu 72301c63fd refactor: LoginPage 继承 MsgBase,统一页面消息追踪机制 2026-06-13 08:59:34 +08:00
Kenan Zhu 609850ab60 ci(workflows): 新增 macOS .app 与 .dmg 打包流程 (#11)
新增 macOS .app 与 .dmg 打包流程
2026-06-10 09:49:54 +08:00
KenanZhu 8e14d45b71 fix(ci): 修复 Windows summary 因 Write-Host 管道失效导致空白输出
pwsh 中 Write-Host 写入信息流(stream 6)不进入管道,Write-Host | Out-File
实际写入空文件。改为直接输出字符串(stream 1)进入管道,与 macOS 的
echo >> GITHUB_STEP_SUMMARY 行为一致。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:17:49 +08:00
KenanZhu f6ef9af39d fix(ci): 修复 macOS 构建流程 6 项缺陷
1. requirements.txt 从 UTF-16 LE 转码为 UTF-8,修复 macOS pip install 解析失败
2. 新增 AutoLibrary_Logo.icns 图标文件,替换 macOS BUNDLE 中不兼容的 .ico 格式
3. PyInstaller 调用改为 python -m PyInstaller,确保 PATH 无关健壮性
4. ddddocr ONNX 模型路径增加多级 fallback 搜索,提升版本兼容性
5. info_plist 移除过时的 CFBundlePackageType,添加 NSHumanReadableCopyright
6. 为 macOS 专属的 sed -i '' 语法添加注释说明

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:05:01 +08:00
KenanZhu 72dce83c55 ci(workflows): 新增 macOS .app 与 .dmg 打包流程 2026-06-09 16:24:37 +08:00
Kenan Zhu c337904010 refactor(*): Page Object 架构迁移、AutoScript 引擎沙箱化与全项目代码规范化 (#9)
Page Object 架构迁移、AutoScript 引擎沙箱化与全项目代码规范化
2026-05-29 14:33:41 +08:00
KenanZhu 779aad13b8 refactor(gui): 简化关于对话框标签文字
SYSTEM INFORMATION → SYSTEM,
PROJECT INFORMATION → PROJECT

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:17:53 +08:00
KenanZhu f3360423e5 fix(build): 重命名 requirement.txt 并统一所有引用
- 重命名 requirement.txt → requirements.txt
- 更新 build.yml 和 build-test.yml 中的 pip cache 和
  install 路径引用
- README.md 恢复 pip install -r requirements.txt 构建步骤

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:16:35 +08:00
KenanZhu bea12d5f0c fix(docs): 更新手册域名并移除不存在的 requirements.txt 引用
- 手册 URL 已从 www.autolibrary.kenanzhu.com/manuals 迁移至
  manuals.autolibrary.kenanzhu.com
- 构建步骤中不再引用已不存在的 requirements.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:12:06 +08:00
KenanZhu b24f39456e fix: 修复 Git 文件名大小写与文件系统不一致的问题
Windows 下 git core.ignorecase=true 导致文件重命名时 Git 无法
检测到大小写变化,推送后服务器上仍为旧命名。通过两步 git mv
强制更新索引,统一所有文件名为规范大小写。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:05:20 +08:00
KenanZhu bb63ee6f03 refactor(gui): 统一 Qt 控件变量命名风格为 PascalCase
将所有 self.xxx 形式的 Qt 控件属性名以及 Qt 对象局部变量由 snake_case
重命名为 PascalCase,提升代码可读性和一致性。涉及 14 个文件,涵盖:
- AutoScript 编排/编辑对话框子模块
- 配置/主窗口/用户树/座位图等核心界面组件
- 定时任务管理相关界面
- 状态标签/浏览器驱动下载对话框

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:35:03 +08:00
KenanZhu 3ebebe015f refactor(gui): 重构关于对话框,改用 QTabWidget 分页展示信息与许可证
将原本的单页文本浏览器替换为 TabWidget,分"关于"和"许可证"两个标签页。
同时优化了信息排版和样式,新增 Selenium 版本展示,移除了 UI 文件中的旧控件。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:34:36 +08:00
KenanZhu 02b3a62868 chore(autoscript): 添加模块版本号 v1.0.0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 16:54:14 +08:00
KenanZhu d7e19dcd52 refactor(pages): 调整预约检查流程顺序,数据校验前置以避免无效浏览器操作
将 ReserveChecker.check(纯数据校验)移至 RecordChecker.canReserve(浏览器查询)之前,
解决 canReserve 在校验前使用未规范化日期的隐式缺陷,并避免无效配置触发昂贵的页面导航操作。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:33:17 +08:00
KenanZhu 59c06b3a19 fix(workflows): 修复图标引用、条件逻辑死代码并统一输出格式
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 02:49:46 +08:00
KenanZhu b78fd2d1e4 chore: 添加 AutoLibrary 应用图标资源
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 01:55:12 +08:00
KenanZhu 2aace40a26 fix(services): 修复验证码识别逻辑、预约时间校验与异常处理结构
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 01:36:28 +08:00
KenanZhu df7ad92f7f fix(pages): 移除裸 except Exception 改用精确异常类型并加固元素操作防护
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 01:36:18 +08:00
KenanZhu 910e3e3224 chore: 统一 __init__.py 许可头为版权声明并改用相对导入
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 01:35:55 +08:00
72 changed files with 2059 additions and 1371 deletions
+289 -16
View File
@@ -7,7 +7,6 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
@@ -15,7 +14,6 @@ on:
- opened
- synchronize
- reopened
# Allow manual trigger for testing
workflow_dispatch:
#
@@ -49,11 +47,13 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirement.txt
pip install -r requirements.txt
- name: Solve ddddocr compatibility and copy model files
run: |
@@ -125,7 +125,7 @@ jobs:
" binaries=[],"
" datas=["
" ('models\\common.onnx', 'ddddocr'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico', 'gui\\resources\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
@@ -153,7 +153,7 @@ jobs:
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico'],"
")"
""
"coll = COLLECT("
@@ -169,9 +169,11 @@ jobs:
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Write-Host "`n========================================"
Write-Host "Generated Main.spec"
Write-Host "========================================"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
Write-Host "========================================`n"
shell: pwsh
- name: Build with PyInstaller
@@ -186,7 +188,7 @@ jobs:
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
"ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) {
@@ -210,12 +212,283 @@ jobs:
- name: Upload build summary
run: |
Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Pull Request #${{ github.event.pull_request.number }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Branch: ${{ github.event.pull_request.head.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
"" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Pull Request #${{ github.event.pull_request.number || 'N/A' }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Branch: ${{ github.event.pull_request.head.ref || github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
shell: pwsh
#
# Build macOS
#
build-macos:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Get version info
id: get_version
run: |
version="pr-test"
tag_name="pr-test"
echo "✓ Mode: Pull Request Test Build"
echo "✓ Tag: $tag_name"
echo "✓ Version: $version"
echo "VERSION=$version" >> $GITHUB_OUTPUT
echo "TAG_NAME=$tag_name" >> $GITHUB_OUTPUT
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Solve ddddocr compatibility and copy model files
run: |
ddddocr_path=$(python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))")
echo "ddddocr package location: $ddddocr_path"
init_file="$ddddocr_path/__init__.py"
if [ -f "$init_file" ]; then
echo "Fixing ddddocr compatibility in: $init_file"
# macOS 使用 BSD sed,要求 -i 后跟备份后缀名(空字符串 = 不备份)
sed -i '' 's/Image\.ANTIALIAS/Image.Resampling.LANCZOS/g' "$init_file"
echo "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
else
echo "✗ ddddocr __init__.py not found"
exit 1
fi
mkdir -p models
echo "✓ Created models directory"
# 多路径 fallback 搜索 ONNX 模型,提升 ddddocr 版本兼容性
onnx_source=""
onnx_dest="models/common.onnx"
candidate_paths=(
"$ddddocr_path/common.onnx"
"$ddddocr_path/models/common.onnx"
"$ddddocr_path/ddddocr/common.onnx"
)
for candidate in "${candidate_paths[@]}"; do
if [ -f "$candidate" ]; then
onnx_source="$candidate"
break
fi
done
# 若以上候选均未命中,回退到全包搜索
if [ -z "$onnx_source" ]; then
echo "⚠ 未在已知路径找到 ONNX 模型,回退到全包搜索..."
onnx_source=$(find "$ddddocr_path" -name "*.onnx" -type f 2>/dev/null | head -1)
fi
if [ -n "$onnx_source" ] && [ -f "$onnx_source" ]; then
cp "$onnx_source" "$onnx_dest"
echo "✓ ONNX model copied: $onnx_source -> $onnx_dest"
else
echo "✗ ONNX model not found in ddddocr package"
echo " Searched directory: $ddddocr_path"
ls -la "$ddddocr_path/" || true
exit 1
fi
if [ -f "$onnx_dest" ]; then
file_size=$(du -h "$onnx_dest" | cut -f1)
echo "✓ Model file verified: $onnx_dest (Size: $file_size)"
else
echo "✗ Failed to copy model file"
exit 1
fi
- name: Compile Qt Resource files
run: |
cd batchs
bash compile_rc.sh
- name: Compile Qt UI files
run: |
cd batchs
bash compile_ui.sh
- name: Generate 'Main.spec'
env:
APP_VERSION: ${{ steps.get_version.outputs.VERSION }}
run: |
version="$APP_VERSION"
exe_name="AutoLibrary-$version"
echo "Generating Main.spec for version: $version"
echo "App name: $exe_name"
printf '%s\n' \
'# -*- mode: python ; coding: utf-8 -*-' \
'' \
'a = Analysis(' \
" ['src/Main.py']," \
' pathex=[],' \
' binaries=[],' \
' datas=[' \
" ('models/common.onnx', 'ddddocr')," \
" ('src/gui/resources/icons/AutoLibrary_Logo_64.ico', 'gui/resources/icons')," \
' ],' \
' hiddenimports=[],' \
' hookspath=[],' \
' hooksconfig={},' \
' runtime_hooks=[],' \
' excludes=[],' \
' noarchive=False,' \
' optimize=0,' \
')' \
'pyz = PYZ(a.pure)' \
'' \
'exe = EXE(' \
' pyz,' \
' a.scripts,' \
' [],' \
' exclude_binaries=True,' \
" name='AutoLibrary'," \
' debug=False,' \
' bootloader_ignore_signals=False,' \
' strip=False,' \
' upx=True,' \
' upx_exclude=[],' \
' runtime_tmpdir=None,' \
' console=False,' \
' disable_windowed_traceback=False,' \
' argv_emulation=False,' \
' target_arch=None,' \
' codesign_identity=None,' \
' entitlements_file=None,' \
" icon=['src/gui/resources/icons/AutoLibrary_Logo_64.ico']," \
')' \
'' \
'coll = COLLECT(' \
' exe,' \
' a.binaries,' \
' a.zipfiles,' \
' a.datas,' \
' strip=False,' \
' upx=True,' \
' upx_exclude=[],' \
" name='${exe_name}'," \
')' \
'' \
'app = BUNDLE(' \
' coll,' \
" name='AutoLibrary.app'," \
" icon='src/gui/resources/icons/AutoLibrary_Logo.icns'," \
" bundle_identifier='com.kenanzhu.autolibrary'," \
' info_plist={' \
" 'NSHighResolutionCapable': 'True'," \
" 'CFBundleName': 'AutoLibrary'," \
" 'CFBundleDisplayName': 'AutoLibrary'," \
" 'CFBundleShortVersionString': '${version}'," \
" 'CFBundleVersion': '${version}'," \
" 'CFBundleExecutable': 'AutoLibrary'," \
" 'NSPrincipalClass': 'NSApplication'," \
" 'LSMinimumSystemVersion': '11.0'," \
" 'NSRequiresAquaSystemAppearance': 'False'," \
" 'NSHumanReadableCopyright': 'Copyright 2025 - 2026 KenanZhu. All rights reserved.'," \
' },' \
')' \
> Main.spec
echo "✓ Main.spec generated successfully"
echo ""
echo "========================================"
echo "Generated Main.spec"
echo "========================================"
cat Main.spec
echo "========================================"
- name: Build with PyInstaller
run: |
python -m PyInstaller Main.spec
- name: Create DMG
run: |
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
app_path="dist/AutoLibrary.app"
if [ ! -d "$app_path" ]; then
echo "✗ App bundle not found: $app_path"
echo "Files in dist directory:"
ls -la dist/
exit 1
fi
echo "Creating DMG from: $app_path"
xattr -cr "$app_path" 2>/dev/null || true
tmp_dmg_dir=$(mktemp -d)
cp -R "$app_path" "$tmp_dmg_dir/"
ln -s /Applications "$tmp_dmg_dir/Applications"
hdiutil create \
-volname "AutoLibrary $tag_name" \
-srcfolder "$tmp_dmg_dir" \
-ov \
-format UDZO \
"$dmg_name"
rm -rf "$tmp_dmg_dir"
if [ -f "$dmg_name" ]; then
dmg_size=$(du -h "$dmg_name" | cut -f1)
echo "✓ DMG created: $dmg_name (Size: $dmg_size)"
else
echo "✗ Failed to create DMG"
exit 1
fi
echo "✓ Artifacts ready:"
echo " - $dmg_name"
echo " - $app_path"
- name: Archive artifacts
uses: actions/upload-artifact@v6
with:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64
path: |
AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64.dmg
dist/AutoLibrary.app/**
retention-days: 7
- name: Upload build summary
run: |
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
version="${{ steps.get_version.outputs.VERSION }}"
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
{
echo "## Build Test Summary (macOS)"
echo ""
echo "========================================"
echo "✓ Pull request build test completed successfully!"
echo "- Version: $version"
echo "- Tag: $tag_name"
echo "- Pull Request #${{ github.event.pull_request.number || 'N/A' }}"
echo "- Branch: ${{ github.event.pull_request.head.ref || github.ref }}"
echo "- Event: ${{ github.event_name }}"
echo "- DMG: $dmg_name"
echo ""
echo "### How to test on macOS"
echo '1. Download the DMG artifact'
echo '2. Open the DMG and drag AutoLibrary.app to /Applications'
echo '3. Right-click → Open the app (to bypass Gatekeeper)'
} >> $GITHUB_STEP_SUMMARY
+325 -19
View File
@@ -1,7 +1,10 @@
name: Build
# This workflow compiles the application for Windows platform using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
# This workflow compiles the application for Windows and macOS platforms using
# PyInstaller, and archives the built artifacts.
#
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
#
# It is triggered when called by the release workflow.
@@ -76,20 +79,22 @@ jobs:
run: |
$versionInfoFile = "src/gui/ALVersionInfo.py"
Write-Host "Verifying $versionInfoFile content:"
Write-Host "=================================="
Write-Host "========================================"
Get-Content $versionInfoFile | Write-Host
Write-Host "=================================="
Write-Host "========================================"
shell: pwsh
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirement.txt
pip install -r requirements.txt
- name: Solve ddddocr compatibility and copy model files
run: |
@@ -161,7 +166,7 @@ jobs:
" binaries=[],"
" datas=["
" ('models\\common.onnx', 'ddddocr'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico', 'gui\\resources\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
@@ -189,7 +194,7 @@ jobs:
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico'],"
")"
""
"coll = COLLECT("
@@ -205,9 +210,11 @@ jobs:
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Write-Host "`n========================================"
Write-Host "Generated Main.spec"
Write-Host "========================================"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
Write-Host "========================================`n"
shell: pwsh
- name: Build with PyInstaller
@@ -222,7 +229,7 @@ jobs:
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
"ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) {
@@ -242,16 +249,315 @@ jobs:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: |
${{ steps.zip_release.outputs.ZIP_NAME }}
retention-days: ${{ github.event_name != 'workflow_call' && 7 || 90 }}
retention-days: ${{ inputs.is_test == 'true' && 7 || 90 }}
- name: Upload build summary
if: ${{ github.event_name != 'workflow_call' }}
if: ${{ inputs.is_test == 'true' }}
run: |
Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
"" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
shell: pwsh
#
# Build macOS
#
build-macos:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Download build version of ALVersionInfo.py
uses: actions/download-artifact@v6
with:
name: updated-version-info-for-build
path: src/gui/
- name: Get version info
id: get_version
run: |
is_test="${{ inputs.is_test }}"
if [ "$is_test" = "true" ]; then
version="test"
tag_name="test"
echo "✓ Mode: Test Build"
else
version="${{ inputs.version }}"
tag_name="${{ inputs.tag_name }}"
if [ -z "$version" ]; then
version="test"
tag_name="test"
echo "✓ Mode: Independent Build (No inputs provided)"
fi
fi
echo "✓ Tag: $tag_name"
echo "✓ Version: $version"
echo "VERSION=$version" >> $GITHUB_OUTPUT
echo "TAG_NAME=$tag_name" >> $GITHUB_OUTPUT
- name: Verify 'ALVersionInfo.py' was updated
run: |
version_info_file="src/gui/ALVersionInfo.py"
echo "Verifying $version_info_file content:"
echo "========================================"
cat "$version_info_file"
echo "========================================"
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Solve ddddocr compatibility and copy model files
run: |
ddddocr_path=$(python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))")
echo "ddddocr package location: $ddddocr_path"
init_file="$ddddocr_path/__init__.py"
if [ -f "$init_file" ]; then
echo "Fixing ddddocr compatibility in: $init_file"
# macOS 使用 BSD sed,要求 -i 后跟备份后缀名(空字符串 = 不备份)
sed -i '' 's/Image\.ANTIALIAS/Image.Resampling.LANCZOS/g' "$init_file"
echo "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
else
echo "✗ ddddocr __init__.py not found"
exit 1
fi
mkdir -p models
echo "✓ Created models directory"
# 多路径 fallback 搜索 ONNX 模型,提升 ddddocr 版本兼容性
onnx_source=""
onnx_dest="models/common.onnx"
candidate_paths=(
"$ddddocr_path/common.onnx"
"$ddddocr_path/models/common.onnx"
"$ddddocr_path/ddddocr/common.onnx"
)
for candidate in "${candidate_paths[@]}"; do
if [ -f "$candidate" ]; then
onnx_source="$candidate"
break
fi
done
# 若以上候选均未命中,回退到全包搜索
if [ -z "$onnx_source" ]; then
echo "⚠ 未在已知路径找到 ONNX 模型,回退到全包搜索..."
onnx_source=$(find "$ddddocr_path" -name "*.onnx" -type f 2>/dev/null | head -1)
fi
if [ -n "$onnx_source" ] && [ -f "$onnx_source" ]; then
cp "$onnx_source" "$onnx_dest"
echo "✓ ONNX model copied: $onnx_source -> $onnx_dest"
else
echo "✗ ONNX model not found in ddddocr package"
echo " Searched directory: $ddddocr_path"
ls -la "$ddddocr_path/" || true
exit 1
fi
if [ -f "$onnx_dest" ]; then
file_size=$(du -h "$onnx_dest" | cut -f1)
echo "✓ Model file verified: $onnx_dest (Size: $file_size)"
else
echo "✗ Failed to copy model file"
exit 1
fi
- name: Compile Qt Resource files
run: |
cd batchs
bash compile_rc.sh
- name: Compile Qt UI files
run: |
cd batchs
bash compile_ui.sh
- name: Generate 'Main.spec'
env:
APP_VERSION: ${{ steps.get_version.outputs.VERSION }}
run: |
version="$APP_VERSION"
exe_name="AutoLibrary-$version"
echo "Generating Main.spec for version: $version"
echo "App name: $exe_name"
printf '%s\n' \
'# -*- mode: python ; coding: utf-8 -*-' \
'' \
'a = Analysis(' \
" ['src/Main.py']," \
' pathex=[],' \
' binaries=[],' \
' datas=[' \
" ('models/common.onnx', 'ddddocr')," \
" ('src/gui/resources/icons/AutoLibrary_Logo_64.ico', 'gui/resources/icons')," \
' ],' \
' hiddenimports=[],' \
' hookspath=[],' \
' hooksconfig={},' \
' runtime_hooks=[],' \
' excludes=[],' \
' noarchive=False,' \
' optimize=0,' \
')' \
'pyz = PYZ(a.pure)' \
'' \
'exe = EXE(' \
' pyz,' \
' a.scripts,' \
' [],' \
' exclude_binaries=True,' \
" name='AutoLibrary'," \
' debug=False,' \
' bootloader_ignore_signals=False,' \
' strip=False,' \
' upx=True,' \
' upx_exclude=[],' \
' runtime_tmpdir=None,' \
' console=False,' \
' disable_windowed_traceback=False,' \
' argv_emulation=False,' \
' target_arch=None,' \
' codesign_identity=None,' \
' entitlements_file=None,' \
" icon=['src/gui/resources/icons/AutoLibrary_Logo_64.ico']," \
')' \
'' \
'coll = COLLECT(' \
' exe,' \
' a.binaries,' \
' a.zipfiles,' \
' a.datas,' \
' strip=False,' \
' upx=True,' \
' upx_exclude=[],' \
" name='${exe_name}'," \
')' \
'' \
'app = BUNDLE(' \
' coll,' \
" name='AutoLibrary.app'," \
" icon='src/gui/resources/icons/AutoLibrary_Logo.icns'," \
" bundle_identifier='com.kenanzhu.autolibrary'," \
' info_plist={' \
" 'NSHighResolutionCapable': 'True'," \
" 'CFBundleName': 'AutoLibrary'," \
" 'CFBundleDisplayName': 'AutoLibrary'," \
" 'CFBundleShortVersionString': '${version}'," \
" 'CFBundleVersion': '${version}'," \
" 'CFBundleExecutable': 'AutoLibrary'," \
" 'NSPrincipalClass': 'NSApplication'," \
" 'LSMinimumSystemVersion': '11.0'," \
" 'NSRequiresAquaSystemAppearance': 'False'," \
" 'NSHumanReadableCopyright': 'Copyright 2025 - 2026 KenanZhu. All rights reserved.'," \
' },' \
')' \
> Main.spec
echo "✓ Main.spec generated successfully"
echo ""
echo "========================================"
echo "Generated Main.spec"
echo "========================================"
cat Main.spec
echo "========================================"
- name: Build with PyInstaller
run: |
python -m PyInstaller Main.spec
- name: Create DMG
run: |
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
app_path="dist/AutoLibrary.app"
if [ ! -d "$app_path" ]; then
echo "✗ App bundle not found: $app_path"
echo "Files in dist directory:"
ls -la dist/
exit 1
fi
echo "Creating DMG from: $app_path"
xattr -cr "$app_path" 2>/dev/null || true
tmp_dmg_dir=$(mktemp -d)
cp -R "$app_path" "$tmp_dmg_dir/"
ln -s /Applications "$tmp_dmg_dir/Applications"
hdiutil create \
-volname "AutoLibrary $tag_name" \
-srcfolder "$tmp_dmg_dir" \
-ov \
-format UDZO \
"$dmg_name"
rm -rf "$tmp_dmg_dir"
if [ -f "$dmg_name" ]; then
dmg_size=$(du -h "$dmg_name" | cut -f1)
echo "✓ DMG created: $dmg_name (Size: $dmg_size)"
else
echo "✗ Failed to create DMG"
exit 1
fi
echo "✓ Artifacts ready:"
echo " - $dmg_name"
echo " - $app_path"
- name: Archive artifacts
uses: actions/upload-artifact@v6
with:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64
path: |
AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-macos-arm64.dmg
dist/AutoLibrary.app/**
retention-days: ${{ inputs.is_test == 'true' && 7 || 90 }}
- name: Upload build summary
if: ${{ inputs.is_test == 'true' }}
run: |
tag_name="${{ steps.get_version.outputs.TAG_NAME }}"
version="${{ steps.get_version.outputs.VERSION }}"
dmg_name="AutoLibrary.$tag_name-macos-arm64.dmg"
{
echo "## Build Summary (macOS)"
echo ""
echo "========================================"
echo "✓ Build test completed successfully!"
echo "- Version: $version"
echo "- Tag: $tag_name"
echo "- Event: ${{ github.event_name }}"
echo "- Ref: ${{ github.ref }}"
echo "- DMG: $dmg_name"
echo ""
echo "### How to test on macOS"
echo '1. Download the DMG artifact'
echo '2. Open the DMG and drag AutoLibrary.app to /Applications'
echo '3. Right-click → Open the app (to bypass Gatekeeper)'
} >> $GITHUB_STEP_SUMMARY
+9 -1
View File
@@ -83,7 +83,9 @@ jobs:
echo "✓ File replaced: $FILE_PATH"
echo ""
echo "Updated file content ==================="
echo "========================================"
echo "Updated file content"
echo "========================================"
cat "$FILE_PATH"
echo "========================================"
@@ -151,3 +153,9 @@ jobs:
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "✓ New commit SHA: $COMMIT_SHA"
echo "## Commit Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "========================================" >> $GITHUB_STEP_SUMMARY
echo "- Version: ${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- Tag: ${{ inputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
echo "- Commit SHA: $COMMIT_SHA" >> $GITHUB_STEP_SUMMARY
+26 -10
View File
@@ -21,8 +21,10 @@ name: Release
# Commits version changes to release branch and creates the release tag.
# 4. Build:
# Compiles the application for Windows platform using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
# Compiles the application for Windows and macOS platforms using PyInstaller, and
# archives the built artifacts.
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
# 5. Release:
# Creates GitHub release with generated artifacts and release notes
@@ -47,7 +49,7 @@ on:
jobs:
#
# Start :
# virtual job that indacates the start of the release process
# virtual job that indicates the start of the release process
#
start:
@@ -158,7 +160,7 @@ jobs:
needs:
- update-version
- commit-release
if: always() && needs.update-version.result == 'success' && needs.commit-release.result == 'success'
if: always() && needs.update-version.result == 'success' && (needs.commit-release.result == 'success' || needs.commit-release.result == 'skipped')
uses: ./.github/workflows/build.yml
permissions:
contents: write
@@ -181,12 +183,18 @@ jobs:
contents: write
steps:
- name: Download artifacts
- name: Download Windows artifacts
uses: actions/download-artifact@v6
with:
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64
path: artifacts/
- name: Download macOS artifacts
uses: actions/download-artifact@v6
with:
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-macos-arm64
path: artifacts/
- name: Create release
id: create_release
uses: softprops/action-gh-release@v2
@@ -195,6 +203,7 @@ jobs:
name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }}
files: |
artifacts/AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64.zip
artifacts/AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-macos-arm64.dmg
draft: false
prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }}
generate_release_notes: true
@@ -205,7 +214,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# End :
# virtual job that indacates the end of the release process
# virtual job that indicates the end of the release process
#
end:
@@ -227,7 +236,7 @@ jobs:
- release
- extract-version
- commit-release
if: ${{ needs.release.result == 'success' && needs.commit-release.result == 'success' }}
if: ${{ needs.release.result == 'success' && (needs.commit-release.result == 'success' || needs.commit-release.result == 'skipped') }}
runs-on: ubuntu-latest
permissions:
contents: write
@@ -267,9 +276,13 @@ jobs:
git checkout ${MAIN_BRANCH}
# Show branch status before merge
echo "=== Branch status before merge ==="
echo "========================================"
echo "Branch status before merge"
echo "========================================"
git log --oneline --graph --all -5
echo "=== Diff between ${MAIN_BRANCH} and origin/${BRANCH_NAME} ==="
echo "========================================"
echo "Diff: ${MAIN_BRANCH} vs origin/${BRANCH_NAME}"
echo "========================================"
git diff ${MAIN_BRANCH} origin/${BRANCH_NAME} --stat || echo "No differences found"
# Force create a merge commit even if there are no changes
@@ -279,7 +292,9 @@ jobs:
-m "chore(release): merge ${BRANCH_NAME} to ${MAIN_BRANCH} [auto release commit]"
# Show merge result
echo "=== Merge result ==="
echo "========================================"
echo "Merge result"
echo "========================================"
git log --oneline --graph -3
# Push to main
@@ -310,6 +325,7 @@ jobs:
echo "## Release Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "========================================" >> $GITHUB_STEP_SUMMARY
echo "✓ Release completed successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Actions Performed:" >> $GITHUB_STEP_SUMMARY
+13 -3
View File
@@ -128,10 +128,15 @@ jobs:
echo "Build version file location: $VER_INFO_BUILDFILE"
echo "Commit version file location: $VER_INFO_COMMITFILE"
echo ""
echo "Build version ALVersionInfo.py content ="
echo "========================================"
echo "Build version ALVersionInfo.py"
echo "========================================"
cat "$VER_INFO_BUILDFILE"
echo "========================================"
echo ""
echo "Commit version ALVersionInfo.py content "
echo "========================================"
echo "Commit version ALVersionInfo.py"
echo "========================================"
cat "$VER_INFO_COMMITFILE"
echo "========================================"
@@ -140,11 +145,16 @@ jobs:
run: |
if git diff --quiet src/gui/ALVersionInfo.py; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "! No changes detected in ALVersionInfo.py"
echo " No changes detected in ALVersionInfo.py"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✓ ALVersionInfo.py has been modified"
fi
echo "## Update Version Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "========================================" >> $GITHUB_STEP_SUMMARY
echo "- Version: ${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
echo "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" >> $GITHUB_STEP_SUMMARY
- name: Upload modified ALVersionInfo.py ready for build
if: steps.check_changes.outputs.has_changes == 'true'
View File
+1 -1
View File
@@ -25,7 +25,7 @@
6. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行,支持设置重复任务
7. 驱动管理 - 内置浏览器驱动自动管理,支持自动检测浏览器版本并下载对应驱动,无需手动下载
*具体操作方法和注意事项请访问我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals)*
*具体操作方法和注意事项请访问我们的 [帮助手册](https://manuals.autolibrary.kenanzhu.com)*
### 如何使用
+3
View File
@@ -0,0 +1,3 @@
This folder is used to store the manuals.
Our manuals are available at https://manuals.autolibrary.kenanzhu.com
-3
View File
@@ -1,3 +0,0 @@
This folder is used to store the manuals.
Our manuals are available at https://www.autolibrary.kenanzhu.com/manuals
BIN
View File
Binary file not shown.
+39
View File
@@ -0,0 +1,39 @@
attrs==26.1.0
certifi==2026.2.25
cffi==2.0.0
charset-normalizer==3.4.6
ddddocr==1.0.6
flatbuffers==25.12.19
h11==0.16.0
idna==3.11
lupa==2.8
mpmath==1.3.0
numpy==2.4.3
onnxruntime==1.24.4
outcome==1.3.0.post0
packaging==26.0
pefile==2024.8.26
pillow==12.1.1
protobuf==7.34.0
pybrowsers==1.3.2
pycparser==3.0
pyinstaller==6.19.0
pyinstaller-hooks-contrib==2026.3
PySide6==6.10.2
PySide6_Addons==6.10.2
PySide6_Essentials==6.10.2
PySocks==1.7.1
pywin32-ctypes==0.2.3
requests==2.32.5
selenium==4.38.0
setuptools==82.0.1
shiboken6==6.10.2
sniffio==1.3.1
sortedcontainers==2.4.0
sympy==1.14.0
trio==0.33.0
trio-websocket==0.12.2
typing_extensions==4.15.0
urllib3==2.6.3
websocket-client==1.9.0
wsproto==1.3.2
+2 -10
View File
@@ -7,17 +7,9 @@ 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 autoscript.ASEngine import ASEngine
__all__ = [
"ASEngine",
"createEngine",
"createMockTargetData",
"createAllVariablesTable",
"createTargetVarDefs",
]
from .ASEngine import ASEngine
__version__ = "1.0.0" # autoscript version
_TARGET_VAR_DEFS = [
("USERNAME", "String", ["username"], "用户名"),
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Base module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- MsgBase: Base class for messages.
"""
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.
"""
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Boot module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- AppInitializer: Application initializer class.
"""
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.
"""
+81 -31
View File
@@ -16,11 +16,16 @@ from PySide6.QtCore import (
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QApplication,
QDialog
QDialog,
QTabWidget,
QTextBrowser
)
from gui.ALVersionInfo import (
AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE
AL_VERSION,
AL_COMMIT_SHA,
AL_COMMIT_DATE,
AL_BUILD_DATE
)
from gui.resources.ui.Ui_ALAboutDialog import Ui_ALAboutDialog
from gui.resources import ALResource
@@ -43,12 +48,23 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
):
self.LogoIconLabel.setPixmap(QIcon(":/res/icons/AutoLibrary_Logo_64.svg").pixmap(48, 48))
info_text = self.generateAboutText()
self.AboutInfoBrowser.setHtml(info_text)
browser_font = self.AboutInfoBrowser.font()
browser_font.setFamily("Courier New")
self.AboutInfoBrowser.setFont(browser_font)
self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
self.TabWidget = QTabWidget()
self.TabWidget.setDocumentMode(True)
AboutBrowser = QTextBrowser()
AboutBrowser.setHtml(self.generateAboutText())
AboutBrowser.setOpenExternalLinks(True)
AboutBrowser.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap)
AboutBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
BrowserFont = AboutBrowser.font()
BrowserFont.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"])
AboutBrowser.setFont(BrowserFont)
self.TabWidget.addTab(AboutBrowser, "关于")
LicenseBrowser = QTextBrowser()
LicenseBrowser.setHtml(self.generateLicenseText())
LicenseBrowser.setOpenExternalLinks(True)
LicenseBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
self.TabWidget.addTab(LicenseBrowser, "许可证")
self.AboutInfoLayout.addWidget(self.TabWidget)
def connectSignals(
self
@@ -61,33 +77,57 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
) -> str:
os_info = self.getOSInfo()
run_on = f"{os_info['system']} {os_info['version']} {os_info['architecture']}"
selenium_ver = self.getSeleniumVersion()
about_text = f"""
<h4>Version Information:</h4>
Version: {AL_VERSION}<br>
<b style="font-size:14px;">VERSION: {AL_VERSION}</b><br>
Commit SHA: {AL_COMMIT_SHA}<br>
Commit date: {AL_COMMIT_DATE}<br>
Build date: {AL_BUILD_DATE}<br>
Python version: {platform.python_version()}<br>
Qt version: {self.getQtVersion()}<br>
<h4>System Information:</h4>
<br>
<b style="font-size:14px;">SYSTEM</b><br>
Running on: {run_on}<br>
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.kenanzhu.com" style="text-decoration: none;">https://www.autolibrary.kenanzhu.com</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>
<br>
<b style="font-size:14px;">DEPENDENCIES</b><br>
Python: {platform.python_version()}<br>
Qt(PySide6): {self.getQtVersion()}<br>
Selenium: {selenium_ver}<br>
<br>
<b style="font-size:14px;">PROJECT</b><br>
Website: <a href="https://www.autolibrary.kenanzhu.com" style="text-decoration:none;">https://www.autolibrary.kenanzhu.com</a><br>
Repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration:none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
<br>
<b style="font-size:14px;">AUTHOR</b><br>
Developer/Maintainer: KenanZhu<br>
Contact: <a href="mailto:nanoki_zh@163.com">nanoki_zh@163.com</a><br>
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration:none;">https://www.github.com/KenanZhu</a><br>
"""
return about_text
def generateLicenseText(
self
) -> str:
return """
<b>MIT License</b>
<p>Copyright &copy; 2025 - 2026 KenanZhu</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.</p>"""
def getOSInfo(
self
):
@@ -129,13 +169,23 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
except:
return "Unknown"
def getSeleniumVersion(
self
):
try:
import selenium
return selenium.__version__
except Exception:
return "Unknown"
def copyAboutInfo(
self
):
about_text = self.AboutInfoBrowser.toPlainText()
clipboard = QApplication.clipboard()
clipboard.setText(about_text)
about_text = self.TabWidget.currentWidget().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))
QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text))
+237 -237
View File
@@ -74,54 +74,54 @@ class ALScriptHighlighter(QSyntaxHighlighter):
super().__init__(parent)
self._rules = []
keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#569CD6"))
keywordFmt.setFontWeight(QFont.Weight.Bold)
KeywordFmt = QTextCharFormat()
KeywordFmt.setForeground(QColor("#569CD6"))
KeywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in [
"if", "elseif", "else", "end", "then",
"and", "or", "not",
"local", "function", "return", "nil",
]:
self._rules.append((r"\b" + kw + r"\b", keywordFmt))
boolFmt = QTextCharFormat()
boolFmt.setForeground(QColor("#4FC1FF"))
boolFmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\btrue\b", boolFmt))
self._rules.append((r"\bfalse\b", boolFmt))
cmpFmt = QTextCharFormat()
cmpFmt.setForeground(QColor("#C586C0"))
cmpFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b" + kw + r"\b", KeywordFmt))
BoolFmt = QTextCharFormat()
BoolFmt.setForeground(QColor("#4FC1FF"))
BoolFmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\btrue\b", BoolFmt))
self._rules.append((r"\bfalse\b", BoolFmt))
CmpFmt = QTextCharFormat()
CmpFmt.setForeground(QColor("#C586C0"))
CmpFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
self._rules.append((op, cmpFmt))
arithFmt = QTextCharFormat()
arithFmt.setForeground(QColor("#C586C0"))
arithFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((op, CmpFmt))
ArithFmt = QTextCharFormat()
ArithFmt.setForeground(QColor("#C586C0"))
ArithFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
self._rules.append((op, arithFmt))
funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#DCDCAA"))
funcFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((op, ArithFmt))
FuncFmt = QTextCharFormat()
FuncFmt.setForeground(QColor("#DCDCAA"))
FuncFmt.setFontWeight(QFont.Weight.Normal)
for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]:
self._rules.append((r"\b" + fn + r"\b", funcFmt))
varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#9CDCFE"))
varFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b" + fn + r"\b", FuncFmt))
VarFmt = QTextCharFormat()
VarFmt.setForeground(QColor("#9CDCFE"))
VarFmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in createAllVariablesTable().items()]
for var in var_names:
self._rules.append((r"\b" + var + r"\b", varFmt))
strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#CE9178"))
strFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', strFmt))
self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#B5CEA8"))
numFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+(?:\.\d+)?\b", numFmt))
commentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#6A9955"))
commentFmt.setFontItalic(True)
self._rules.append((r"--[^\n]*", commentFmt))
self._rules.append((r"\b" + var + r"\b", VarFmt))
StrFmt = QTextCharFormat()
StrFmt.setForeground(QColor("#CE9178"))
StrFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', StrFmt))
self._rules.append((r"'[^']*'", StrFmt))
NumFmt = QTextCharFormat()
NumFmt.setForeground(QColor("#B5CEA8"))
NumFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+(?:\.\d+)?\b", NumFmt))
CommentFmt = QTextCharFormat()
CommentFmt.setForeground(QColor("#6A9955"))
CommentFmt.setFontItalic(True)
self._rules.append((r"--[^\n]*", CommentFmt))
def highlightBlock(
self,
@@ -147,22 +147,22 @@ class _DebugResultDialog(QDialog):
super().__init__(parent)
self.setWindowTitle("调试运行结果 - AutoLibrary")
self.setMinimumSize(600, 200)
layout = QVBoxLayout(self)
table = QTableWidget(len(changes), 3)
table.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"])
table.horizontalHeader().setStretchLastSection(True)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
DbgLayout = QVBoxLayout(self)
DbgTable = QTableWidget(len(changes), 3)
DbgTable.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"])
DbgTable.horizontalHeader().setStretchLastSection(True)
DbgTable.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
DbgTable.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes):
label = f"{display_name}: {name}({var_type})"
table.setItem(row, 0, QTableWidgetItem(label))
table.setItem(row, 1, QTableWidgetItem(str(before_val)))
table.setItem(row, 2, QTableWidgetItem(str(after_val)))
layout.addWidget(table)
btnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
btnBox.accepted.connect(self.accept)
layout.addWidget(btnBox)
DbgTable.setItem(row, 0, QTableWidgetItem(label))
DbgTable.setItem(row, 1, QTableWidgetItem(str(before_val)))
DbgTable.setItem(row, 2, QTableWidgetItem(str(after_val)))
DbgLayout.addWidget(DbgTable)
DbgBtnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
DbgBtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
DbgBtnBox.accepted.connect(self.accept)
DbgLayout.addWidget(DbgBtnBox)
class _TabToSpacesEditor(QPlainTextEdit):
@@ -193,9 +193,9 @@ class ALAutoScriptEditDialog(QDialog):
self.setupUi()
self.connectSignals()
self.textEdit.setPlainText(script)
self._highlighter = ALScriptHighlighter(
self.textEdit.document()
self.TextEdit.setPlainText(script)
self._Highlighter = ALScriptHighlighter(
self.TextEdit.document()
)
if mockData:
self.setMockData(mockData)
@@ -206,80 +206,80 @@ class ALAutoScriptEditDialog(QDialog):
self.setWindowTitle("AutoScript 编辑 - AutoLibrary")
self.setMinimumSize(660, 600)
layout = QVBoxLayout(self)
layout.setSpacing(3)
layout.setContentsMargins(3, 3, 3, 3)
toolbarLayout = QHBoxLayout()
self.zoomInBtn = QPushButton("")
self.zoomInBtn.setFixedSize(25, 25)
self.zoomOutBtn = QPushButton("")
self.zoomOutBtn.setFixedSize(25, 25)
self.zoomResetBtn = QPushButton("")
self.zoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
self.zoomResetBtn.setIconSize(QSize(20, 20))
self.zoomResetBtn.setFixedSize(25, 25)
self.zoomResetBtn.setToolTip("重置缩放")
self.zoomLabel = QLabel(f"{self._fontSize}px")
self.zoomLabel.setFixedHeight(25)
self.orchBtn = QPushButton("编排")
self.orchBtn.setFixedHeight(25)
self.orchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
toolbarLayout.addWidget(self.orchBtn)
self.debugBtn = QPushButton("▶ 调试运行")
self.debugBtn.setFixedHeight(25)
self.debugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
toolbarLayout.addWidget(self.debugBtn)
sep = QFrame()
sep.setFrameShape(QFrame.Shape.VLine)
sep.setFrameShadow(QFrame.Shadow.Sunken)
sep.setFixedWidth(1)
toolbarLayout.addWidget(sep)
toolbarLayout.addWidget(self.zoomInBtn)
toolbarLayout.addWidget(self.zoomOutBtn)
toolbarLayout.addWidget(self.zoomResetBtn)
toolbarLayout.addWidget(self.zoomLabel)
toolbarLayout.addStretch()
self.copyBtn = QPushButton("")
self.copyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
self.copyBtn.setIconSize(QSize(20, 20))
self.copyBtn.setFixedSize(25, 25)
self.copyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self.copyBtn)
layout.addLayout(toolbarLayout)
self.textEdit = _TabToSpacesEditor(self)
self.textEdit.setTabStopDistance(40)
self.textEdit.setLineWrapMode(
Layout = QVBoxLayout(self)
Layout.setSpacing(3)
Layout.setContentsMargins(3, 3, 3, 3)
ToolbarLayout = QHBoxLayout()
self.ZoomInBtn = QPushButton("")
self.ZoomInBtn.setFixedSize(25, 25)
self.ZoomOutBtn = QPushButton("")
self.ZoomOutBtn.setFixedSize(25, 25)
self.ZoomResetBtn = QPushButton("")
self.ZoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
self.ZoomResetBtn.setIconSize(QSize(20, 20))
self.ZoomResetBtn.setFixedSize(25, 25)
self.ZoomResetBtn.setToolTip("重置缩放")
self.ZoomLabel = QLabel(f"{self._fontSize}px")
self.ZoomLabel.setFixedHeight(25)
self.OrchBtn = QPushButton("编排")
self.OrchBtn.setFixedHeight(25)
self.OrchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
ToolbarLayout.addWidget(self.OrchBtn)
self.DebugBtn = QPushButton("▶ 调试运行")
self.DebugBtn.setFixedHeight(25)
self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
ToolbarLayout.addWidget(self.DebugBtn)
Sep = QFrame()
Sep.setFrameShape(QFrame.Shape.VLine)
Sep.setFrameShadow(QFrame.Shadow.Sunken)
Sep.setFixedWidth(1)
ToolbarLayout.addWidget(Sep)
ToolbarLayout.addWidget(self.ZoomInBtn)
ToolbarLayout.addWidget(self.ZoomOutBtn)
ToolbarLayout.addWidget(self.ZoomResetBtn)
ToolbarLayout.addWidget(self.ZoomLabel)
ToolbarLayout.addStretch()
self.CopyBtn = QPushButton("")
self.CopyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
self.CopyBtn.setIconSize(QSize(20, 20))
self.CopyBtn.setFixedSize(25, 25)
self.CopyBtn.setToolTip("复制脚本")
ToolbarLayout.addWidget(self.CopyBtn)
Layout.addLayout(ToolbarLayout)
self.TextEdit = _TabToSpacesEditor(self)
self.TextEdit.setTabStopDistance(40)
self.TextEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap
)
self.textEdit.setStyleSheet(
self.TextEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
layout.addWidget(self.textEdit)
self.createButtonPanel(layout)
self.btnBox = QDialogButtonBox(
Layout.addWidget(self.TextEdit)
self.createButtonPanel(Layout)
self.BtnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
layout.addWidget(self.btnBox)
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
Layout.addWidget(self.BtnBox)
def createButtonPanel(
self,
parent_layout
ParentLayout
):
splitter = QSplitter(Qt.Orientation.Horizontal)
tabWidget = QTabWidget()
tabWidget.setMaximumHeight(150)
basicWidget = QWidget()
basicLayout = QGridLayout(basicWidget)
basicLayout.setSpacing(4)
basicLayout.setContentsMargins(4, 4, 4, 4)
basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
Splitter = QSplitter(Qt.Orientation.Horizontal)
TabWidget = QTabWidget()
TabWidget.setMaximumHeight(150)
BasicWidget = QWidget()
BasicLayout = QGridLayout(BasicWidget)
BasicLayout.setSpacing(4)
BasicLayout.setContentsMargins(4, 4, 4, 4)
BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [
("如果 (if...)", "if then\n \nend"),
("再如果 (elseif...)", "elseif then\n "),
@@ -287,22 +287,22 @@ class ALAutoScriptEditDialog(QDialog):
("结束 (end)", "end"),
("跳过 (pass)", "-- pass"),
]
self.addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3)
self.addButtonsToGrid(BasicLayout, controlButtons, 0, 0, 3)
assignButtons = [
("赋值 (=)", " = "),
]
self.addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3)
tabWidget.addTab(basicWidget, "基本语法")
operatorWidget = QWidget()
operatorLayout = QGridLayout(operatorWidget)
operatorLayout.setSpacing(4)
operatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.addButtonsToGrid(BasicLayout, assignButtons, 1, 2, 3)
TabWidget.addTab(BasicWidget, "基本语法")
OperatorWidget = QWidget()
OperatorLayout = QGridLayout(OperatorWidget)
OperatorLayout.setSpacing(4)
OperatorLayout.setContentsMargins(4, 4, 4, 4)
OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [
("加 (+)", " + "),
("减 (-)", " - "),
]
self.addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3)
self.addButtonsToGrid(OperatorLayout, arithmeticButtons, 0, 0, 3)
compareButtons = [
("等于 (==)", " == "),
("不等于 (~=)", " ~= "),
@@ -311,50 +311,50 @@ class ALAutoScriptEditDialog(QDialog):
("大于等于 (>=)", " >= "),
("小于等于 (<=)", " <= "),
]
self.addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3)
self.addButtonsToGrid(OperatorLayout, compareButtons, 1, 0, 3)
logic_buttons = [
("且 (and)", " and "),
("或 (or)", " or "),
]
self.addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3)
tabWidget.addTab(operatorWidget, "运算符")
literalWidget = QWidget()
literalLayout = QGridLayout(literalWidget)
literalLayout.setSpacing(4)
literalLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.addButtonsToGrid(OperatorLayout, logic_buttons, 2, 0, 3)
TabWidget.addTab(OperatorWidget, "运算符")
LiteralWidget = QWidget()
LiteralLayout = QGridLayout(LiteralWidget)
LiteralLayout.setSpacing(4)
LiteralLayout.setContentsMargins(4, 4, 4, 4)
LiteralLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [
("真 (true)", "true"),
("假 (false)", "false"),
]
self.addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3)
self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3)
dateTimeButtons = [
("日期", 'date(2026, 1, 1)'),
("时间", 'time(0, 0)'),
]
self.addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3)
self.addButtonsToGrid(LiteralLayout, dateTimeButtons, 1, 0, 3)
hintButtons = [
("字符串", '"请输入文本"'),
("数字", "123"),
("注释", "-- 请输入注释"),
]
self.addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3)
tabWidget.addTab(literalWidget, "字面量")
varWidget = QWidget()
varLayout = QGridLayout(varWidget)
varLayout.setSpacing(4)
varLayout.setContentsMargins(4, 4, 4, 4)
varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.addButtonsToGrid(LiteralLayout, hintButtons, 2, 0, 3)
TabWidget.addTab(LiteralWidget, "字面量")
VarWidget = QWidget()
VarLayout = QGridLayout(VarWidget)
VarLayout.setSpacing(4)
VarLayout.setContentsMargins(4, 4, 4, 4)
VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
varButtons = [
(display_name, name) for display_name, (name, _) in createAllVariablesTable().items()
]
self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3)
tabWidget.addTab(varWidget, "变量")
funcWidget = QWidget()
funcLayout = QGridLayout(funcWidget)
funcLayout.setSpacing(4)
funcLayout.setContentsMargins(4, 4, 4, 4)
funcLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.addButtonsToGrid(VarLayout, varButtons, 0, 0, 3)
TabWidget.addTab(VarWidget, "变量")
FuncWidget = QWidget()
FuncLayout = QGridLayout(FuncWidget)
FuncLayout.setSpacing(4)
FuncLayout.setContentsMargins(4, 4, 4, 4)
FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
funcButtons = [
("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"),
("timenow()", "timenow()", "返回当前时间在一天中的分钟数"),
@@ -362,22 +362,22 @@ class ALAutoScriptEditDialog(QDialog):
("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"),
]
for i, (text, template, tooltip) in enumerate(funcButtons):
btn = QPushButton(text)
btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100)
btn.setFixedHeight(25)
btn.setToolTip(tooltip)
funcLayout.addWidget(btn, i // 2, i % 2)
tabWidget.addTab(funcWidget, "工具函数")
mockPanel = self.createMockPanel()
mockPanel.setMinimumWidth(260)
splitter.addWidget(tabWidget)
splitter.addWidget(mockPanel)
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 1)
splitter.setSizes([530, 530])
parent_layout.addWidget(splitter)
Btn = QPushButton(text)
Btn.setProperty("template", template)
Btn.clicked.connect(self.insertTemplate)
Btn.setFixedWidth(100)
Btn.setFixedHeight(25)
Btn.setToolTip(tooltip)
FuncLayout.addWidget(Btn, i // 2, i % 2)
TabWidget.addTab(FuncWidget, "工具函数")
MockPanel = self.createMockPanel()
MockPanel.setMinimumWidth(260)
Splitter.addWidget(TabWidget)
Splitter.addWidget(MockPanel)
Splitter.setStretchFactor(0, 1)
Splitter.setStretchFactor(1, 1)
Splitter.setSizes([530, 530])
ParentLayout.addWidget(Splitter)
def addButtonsToGrid(
self,
@@ -392,13 +392,13 @@ class ALAutoScriptEditDialog(QDialog):
row = start_row
for btn_text, template in buttons:
btn = QPushButton(btn_text)
btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100)
btn.setFixedHeight(25)
btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(btn, row, col)
Btn = QPushButton(btn_text)
Btn.setProperty("template", template)
Btn.clicked.connect(self.insertTemplate)
Btn.setFixedWidth(100)
Btn.setFixedHeight(25)
Btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(Btn, row, col)
col += 1
if col >= start_col + max_columns:
col = start_col
@@ -408,10 +408,10 @@ class ALAutoScriptEditDialog(QDialog):
self
) -> QGroupBox:
group = QGroupBox("模拟目标数据")
form = QFormLayout(group)
form.setSpacing(4)
form.setContentsMargins(5, 10, 5, 5)
Group = QGroupBox("模拟目标数据")
Form = QFormLayout(Group)
Form.setSpacing(4)
Form.setContentsMargins(5, 10, 5, 5)
self._mockWidgets = {}
mockData = createMockTargetData()
for name, var_type, key_path, display_name in createTargetVarDefs():
@@ -419,11 +419,11 @@ class ALAutoScriptEditDialog(QDialog):
for key in key_path:
d = d[key]
default = d
widget = self.makeMockInput(var_type, default)
label = QLabel(f"{display_name}: {name}({var_type})")
form.addRow(label, widget)
self._mockWidgets[name] = (widget, var_type, key_path)
return group
Widget = self.makeMockInput(var_type, default)
Label = QLabel(f"{display_name}: {name}({var_type})")
Form.addRow(Label, Widget)
self._mockWidgets[name] = (Widget, var_type, key_path)
return Group
def makeMockInput(
self,
@@ -432,41 +432,41 @@ class ALAutoScriptEditDialog(QDialog):
) -> QWidget:
if var_type == "String":
w = QLineEdit()
w.setText(str(default))
return w
W = QLineEdit()
W.setText(str(default))
return W
if var_type == "Boolean":
w = QComboBox()
w.addItems(["", ""])
w.setCurrentIndex(0 if default else 1)
return w
W = QComboBox()
W.addItems(["", ""])
W.setCurrentIndex(0 if default else 1)
return W
if var_type == "Date":
w = QDateEdit()
w.setCalendarPopup(True)
w.setDisplayFormat("yyyy-MM-dd")
w.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
return w
W = QDateEdit()
W.setCalendarPopup(True)
W.setDisplayFormat("yyyy-MM-dd")
W.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
return W
if var_type == "Time":
w = QTimeEdit()
w.setDisplayFormat("HH:mm")
w.setTime(QTime.fromString(str(default), "HH:mm"))
return w
W = QTimeEdit()
W.setDisplayFormat("HH:mm")
W.setTime(QTime.fromString(str(default), "HH:mm"))
return W
if var_type == "Int":
w = QSpinBox()
w.setMinimum(-999999)
w.setMaximum(999999)
w.setValue(int(default) if default else 0)
return w
W = QSpinBox()
W.setMinimum(-999999)
W.setMaximum(999999)
W.setValue(int(default) if default else 0)
return W
if var_type == "Float":
w = QDoubleSpinBox()
w.setMinimum(-999999.0)
w.setMaximum(999999.0)
w.setDecimals(2)
w.setValue(float(default) if default else 0.0)
return w
w = QLineEdit()
w.setText(str(default))
return w
W = QDoubleSpinBox()
W.setMinimum(-999999.0)
W.setMaximum(999999.0)
W.setDecimals(2)
W.setValue(float(default) if default else 0.0)
return W
W = QLineEdit()
W.setText(str(default))
return W
def getMockData(
self
@@ -541,45 +541,45 @@ class ALAutoScriptEditDialog(QDialog):
self
):
self.btnBox.accepted.connect(self.accept)
self.btnBox.rejected.connect(self.reject)
self.orchBtn.clicked.connect(self.onOpenOrchDialog)
self.debugBtn.clicked.connect(self.onDebugRun)
self.zoomInBtn.clicked.connect(self.onZoomIn)
self.zoomOutBtn.clicked.connect(self.onZoomOut)
self.zoomResetBtn.clicked.connect(self.onZoomReset)
self.copyBtn.clicked.connect(self.onCopy)
self.BtnBox.accepted.connect(self.accept)
self.BtnBox.rejected.connect(self.reject)
self.OrchBtn.clicked.connect(self.onOpenOrchDialog)
self.DebugBtn.clicked.connect(self.onDebugRun)
self.ZoomInBtn.clicked.connect(self.onZoomIn)
self.ZoomOutBtn.clicked.connect(self.onZoomOut)
self.ZoomResetBtn.clicked.connect(self.onZoomReset)
self.CopyBtn.clicked.connect(self.onCopy)
def getScript(
self
) -> str:
return self.textEdit.toPlainText()
return self.TextEdit.toPlainText()
def updateFontSize(
self
):
self.textEdit.setStyleSheet(
self.TextEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
self.zoomLabel.setText(f"{self._fontSize}px")
self.ZoomLabel.setText(f"{self._fontSize}px")
@Slot()
def insertTemplate(
self
):
btn = self.sender()
if not isinstance(btn, QPushButton):
Btn = self.sender()
if not isinstance(Btn, QPushButton):
return
template = btn.property("template")
template = Btn.property("template")
if not template:
return
cursor = self.textEdit.textCursor()
cursor = self.TextEdit.textCursor()
cursor.insertText(template)
@Slot()
@@ -611,11 +611,11 @@ class ALAutoScriptEditDialog(QDialog):
self
):
clipboard = QApplication.clipboard()
clipboard.setText(self.textEdit.toPlainText())
self.copyBtn.setEnabled(False)
Clipboard = QApplication.clipboard()
Clipboard.setText(self.TextEdit.toPlainText())
self.CopyBtn.setEnabled(False)
QTimer.singleShot(2000, lambda: (
self.copyBtn.setEnabled(True)
self.CopyBtn.setEnabled(True)
))
@Slot()
@@ -624,20 +624,20 @@ class ALAutoScriptEditDialog(QDialog):
):
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
dlg = ALAutoScriptOrchDialog(self)
if dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript()
Dlg = ALAutoScriptOrchDialog(self)
if Dlg.exec() == QDialog.DialogCode.Accepted:
script = Dlg.getScript()
if script:
cursor = self.textEdit.textCursor()
cursor = self.TextEdit.textCursor()
cursor.insertText(script)
dlg.deleteLater()
Dlg.deleteLater()
@Slot()
def onDebugRun(
self
):
script = self.textEdit.toPlainText().strip()
script = self.TextEdit.toPlainText().strip()
if not script:
QMessageBox.warning(self, "提示", "脚本内容为空。")
return
@@ -664,6 +664,6 @@ class ALAutoScriptEditDialog(QDialog):
if not changes:
QMessageBox.information(self, "调试运行", "目标变量未发生变化。")
return
dlg = _DebugResultDialog(changes, self)
dlg.exec()
dlg.deleteLater()
Dlg = _DebugResultDialog(changes, self)
Dlg.exec()
Dlg.deleteLater()
+9 -2
View File
@@ -1,3 +1,10 @@
from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
__all__ = ["ALAutoScriptOrchDialog"]
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 ._dialog import ALAutoScriptOrchDialog
+64 -64
View File
@@ -57,81 +57,81 @@ class ConditionalBlock(QGroupBox):
"margin-top: 5px; padding-top: 5px; }"
)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout()
headerLayout.setSpacing(8)
self.typeCombo = QComboBox(self)
self.typeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE")
self.typeCombo.setFixedHeight(25)
MainLayout = QVBoxLayout(self)
MainLayout.setSpacing(6)
MainLayout.setContentsMargins(8, 8, 8, 8)
HeaderLayout = QHBoxLayout()
HeaderLayout.setSpacing(8)
self.TypeCombo = QComboBox(self)
self.TypeCombo.addItem("IF", "IF")
self.TypeCombo.addItem("ELSE IF", "ELSE IF")
self.TypeCombo.addItem("ELSE", "ELSE")
self.TypeCombo.setFixedHeight(25)
if self.blockIndex == 0:
self.typeCombo.setEnabled(False)
headerLayout.addWidget(QLabel("类型:", self))
headerLayout.addWidget(self.typeCombo)
headerLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块", self)
self.deleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy(
self.TypeCombo.setEnabled(False)
HeaderLayout.addWidget(QLabel("类型:", self))
HeaderLayout.addWidget(self.TypeCombo)
HeaderLayout.addStretch()
self.DeleteBlockBtn = QPushButton("删除此块", self)
self.DeleteBlockBtn.setStyleSheet("color: red;")
self.DeleteBlockBtn.setFixedHeight(25)
HeaderLayout.addWidget(self.DeleteBlockBtn)
MainLayout.addLayout(HeaderLayout)
self.ConditionWidget = QWidget(self)
self.ConditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred
)
condLayout = QVBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6)
self.condRowsLayout = QVBoxLayout()
self.condRowsLayout.setSpacing(4)
condLayout.addLayout(self.condRowsLayout)
self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget)
self.addCondBtn.setFixedHeight(25)
condLayout.addWidget(self.addCondBtn)
mainLayout.addWidget(self.conditionWidget)
self.actionLabel = QLabel("执行步骤:", self)
self.actionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel)
self.actionsLayout = QVBoxLayout()
self.actionsLayout.setSpacing(4)
mainLayout.addLayout(self.actionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤", self)
self.addActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn)
CondLayout = QVBoxLayout(self.ConditionWidget)
CondLayout.setContentsMargins(4, 4, 4, 4)
CondLayout.setSpacing(6)
self.CondRowsLayout = QVBoxLayout()
self.CondRowsLayout.setSpacing(4)
CondLayout.addLayout(self.CondRowsLayout)
self.AddCondBtn = QPushButton("+ 添加条件", self.ConditionWidget)
self.AddCondBtn.setFixedHeight(25)
CondLayout.addWidget(self.AddCondBtn)
MainLayout.addWidget(self.ConditionWidget)
self.ActionLabel = QLabel("执行步骤:", self)
self.ActionLabel.setFixedHeight(25)
MainLayout.addWidget(self.ActionLabel)
self.ActionsLayout = QVBoxLayout()
self.ActionsLayout.setSpacing(4)
MainLayout.addLayout(self.ActionsLayout)
self.AddActionBtn = QPushButton("+ 添加执行步骤", self)
self.AddActionBtn.setFixedHeight(25)
MainLayout.addWidget(self.AddActionBtn)
self.setUpdatesEnabled(True)
def connectSignals(
self
):
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addCondBtn.clicked.connect(self.addConditionRow)
self.addActionBtn.clicked.connect(self.addActionStep)
self.TypeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.AddCondBtn.clicked.connect(self.addConditionRow)
self.AddActionBtn.clicked.connect(self.addActionStep)
def addInitialConditionRow(
self
):
row = ConditionRowFrame(
Row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=True, parent=self
)
self._conditionRows.append(row)
self.condRowsLayout.addWidget(row)
self._conditionRows.append(Row)
self.CondRowsLayout.addWidget(Row)
def addConditionRow(
self
):
row = ConditionRowFrame(
Row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=False, parent=self
)
row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row))
self._conditionRows.append(row)
self.condRowsLayout.addWidget(row)
Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row))
self._conditionRows.append(Row)
self.CondRowsLayout.addWidget(Row)
def removeConditionRow(
self,
@@ -140,7 +140,7 @@ class ConditionalBlock(QGroupBox):
if row in self._conditionRows and len(self._conditionRows) > 1:
self._conditionRows.remove(row)
self.condRowsLayout.removeWidget(row)
self.CondRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
@@ -148,10 +148,10 @@ class ConditionalBlock(QGroupBox):
self
):
step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
self._actionWidgets.append(step)
self.actionsLayout.addWidget(step)
Step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
Step.DeleteBtn.clicked.connect(lambda: self.removeActionStep(Step))
self._actionWidgets.append(Step)
self.ActionsLayout.addWidget(Step)
def removeActionStep(
self,
@@ -160,7 +160,7 @@ class ConditionalBlock(QGroupBox):
if step in self._actionWidgets:
self._actionWidgets.remove(step)
self.actionsLayout.removeWidget(step)
self.ActionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
@@ -168,7 +168,7 @@ class ConditionalBlock(QGroupBox):
self
) -> str:
return self.typeCombo.currentData()
return self.TypeCombo.currentData()
def getConditionRows(
self
@@ -239,18 +239,18 @@ class ConditionalBlock(QGroupBox):
prevType: str | None
):
model = self.typeCombo.model()
model = self.TypeCombo.model()
if model is None:
return
for data in ("ELSE IF", "ELSE"):
idx = self.typeCombo.findData(data)
idx = self.TypeCombo.findData(data)
if idx < 0:
continue
item = model.item(idx)
shouldEnable = prevType != "ELSE"
item.setEnabled(shouldEnable)
if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"):
self.typeCombo.setCurrentIndex(0)
if prevType == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"):
self.TypeCombo.setCurrentIndex(0)
@Slot(int)
def onTypeChanged(
@@ -258,8 +258,8 @@ class ConditionalBlock(QGroupBox):
_idx
):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond)
self.actionLabel.setText(
isCond = self.TypeCombo.currentData() in ("IF", "ELSE IF")
self.ConditionWidget.setVisible(isCond)
self.ActionLabel.setText(
"执行步骤:" if isCond else "ELSE 执行步骤:"
)
+35 -35
View File
@@ -40,7 +40,7 @@ class ALAutoScriptOrchDialog(QDialog):
self.setupUi()
self.connectSignals()
self.addBlock()
self.scrollLayout.addStretch()
self.ScrollLayout.addStretch()
def setupUi(
self
@@ -49,33 +49,33 @@ class ALAutoScriptOrchDialog(QDialog):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(640, 600)
self.setModal(True)
mainLayout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.NoFrame)
scrollContent = QWidget()
self.scrollLayout = QVBoxLayout(scrollContent)
self.scrollLayout.setSpacing(5)
scroll.setWidget(scrollContent)
mainLayout.addWidget(scroll)
self.addBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25)
mainLayout.addWidget(self.addBlockBtn)
self.btnBox = QDialogButtonBox(
MainLayout = QVBoxLayout(self)
Scroll = QScrollArea()
Scroll.setWidgetResizable(True)
Scroll.setFrameShape(QFrame.NoFrame)
ScrollContent = QWidget()
self.ScrollLayout = QVBoxLayout(ScrollContent)
self.ScrollLayout.setSpacing(5)
Scroll.setWidget(ScrollContent)
MainLayout.addWidget(Scroll)
self.AddBlockBtn = QPushButton("+ 添加判断块")
self.AddBlockBtn.setFixedHeight(25)
MainLayout.addWidget(self.AddBlockBtn)
self.BtnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox)
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
MainLayout.addWidget(self.BtnBox)
def connectSignals(
self
):
self.btnBox.accepted.connect(self.onAccept)
self.btnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock)
self.BtnBox.accepted.connect(self.onAccept)
self.BtnBox.rejected.connect(self.reject)
self.AddBlockBtn.clicked.connect(self.addBlock)
def updateBlockTypeRestrictions(
self
@@ -90,24 +90,24 @@ class ALAutoScriptOrchDialog(QDialog):
self
):
block = ConditionalBlock(
Block = ConditionalBlock(
len(self._blocks), self._varMgr, parent=self
)
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
block.typeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
block.addActionStep()
self._blocks.append(block)
Block.DeleteBlockBtn.clicked.connect(lambda: self.removeBlock(Block))
Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
Block.addActionStep()
self._blocks.append(Block)
self.updateBlockTypeRestrictions()
if self.scrollLayout.count() > 0:
lastItem = self.scrollLayout.itemAt(
self.scrollLayout.count() - 1
if self.ScrollLayout.count() > 0:
lastItem = self.ScrollLayout.itemAt(
self.ScrollLayout.count() - 1
)
if lastItem and lastItem.spacerItem():
self.scrollLayout.insertWidget(
self.scrollLayout.count() - 1, block
self.ScrollLayout.insertWidget(
self.ScrollLayout.count() - 1, Block
)
return
self.scrollLayout.addWidget(block)
self.ScrollLayout.addWidget(Block)
def removeBlock(
self,
@@ -119,16 +119,16 @@ class ALAutoScriptOrchDialog(QDialog):
return
if block in self._blocks:
self._blocks.remove(block)
self.scrollLayout.removeWidget(block)
self.ScrollLayout.removeWidget(block)
block.hide()
block.deleteLater()
for i, blk in enumerate(self._blocks):
blk.blockIndex = i
if i == 0:
blk.typeCombo.setEnabled(False)
blk.typeCombo.setCurrentIndex(0)
blk.TypeCombo.setEnabled(False)
blk.TypeCombo.setCurrentIndex(0)
else:
blk.typeCombo.setEnabled(True)
blk.TypeCombo.setEnabled(True)
blk.refreshVarCombos()
self.updateBlockTypeRestrictions()
+68 -68
View File
@@ -110,39 +110,39 @@ class _DateInputContainer(QWidget):
self
):
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
self._modeCombo = QComboBox(self)
self._modeCombo.addItem("相对日期", "relative")
self._modeCombo.addItem("绝对日期", "absolute")
self._modeCombo.setFixedHeight(25)
self._stack = QStackedWidget(self)
self._relCombo = QComboBox(self)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
Layout.setSpacing(4)
self._ModeCombo = QComboBox(self)
self._ModeCombo.addItem("相对日期", "relative")
self._ModeCombo.addItem("绝对日期", "absolute")
self._ModeCombo.setFixedHeight(25)
self._Stack = QStackedWidget(self)
self._RelCombo = QComboBox(self)
for display, data in DATE_OPTIONS:
self._relCombo.addItem(display, data)
self._relCombo.setFixedHeight(25)
self._stack.addWidget(self._relCombo)
self._dateEdit = QDateEdit(self)
self._dateEdit.setDisplayFormat("yyyy-MM-dd")
self._dateEdit.setCalendarPopup(True)
self._dateEdit.setFixedHeight(25)
self._stack.addWidget(self._dateEdit)
self._modeCombo.currentIndexChanged.connect(
lambda i: self._stack.setCurrentIndex(i)
self._RelCombo.addItem(display, data)
self._RelCombo.setFixedHeight(25)
self._Stack.addWidget(self._RelCombo)
self._DateEdit = QDateEdit(self)
self._DateEdit.setDisplayFormat("yyyy-MM-dd")
self._DateEdit.setCalendarPopup(True)
self._DateEdit.setFixedHeight(25)
self._Stack.addWidget(self._DateEdit)
self._ModeCombo.currentIndexChanged.connect(
lambda i: self._Stack.setCurrentIndex(i)
)
layout.addWidget(self._modeCombo)
layout.addWidget(self._stack)
layout.addStretch()
Layout.addWidget(self._ModeCombo)
Layout.addWidget(self._Stack)
Layout.addStretch()
def getValue(
self
) -> str:
mode = self._modeCombo.currentData()
mode = self._ModeCombo.currentData()
if mode == "relative":
return self._relCombo.currentText()
return self._dateEdit.date().toString("yyyy-MM-dd")
return self._RelCombo.currentText()
return self._DateEdit.date().toString("yyyy-MM-dd")
class _TimeInputContainer(QWidget):
@@ -153,19 +153,19 @@ class _TimeInputContainer(QWidget):
):
super().__init__(parent)
self._timeEdit = QTimeEdit(self)
self._timeEdit.setDisplayFormat("HH:mm")
self._timeEdit.setFixedHeight(25)
self._TimeEdit = QTimeEdit(self)
self._TimeEdit.setDisplayFormat("HH:mm")
self._TimeEdit.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._timeEdit)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
Layout.addWidget(self._TimeEdit)
def getValue(
self
) -> str:
return self._timeEdit.time().toString("HH:mm")
return self._TimeEdit.time().toString("HH:mm")
class _DateOffsetContainer(QWidget):
@@ -176,20 +176,20 @@ class _DateOffsetContainer(QWidget):
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setFixedHeight(25)
self._unitCombo = QComboBox(self)
self._SpinBox = QSpinBox(self)
self._SpinBox.setRange(0, 99999)
self._SpinBox.setFixedHeight(25)
self._UnitCombo = QComboBox(self)
for display, data in DATE_OFFSET_OPTIONS:
self._unitCombo.addItem(display, data)
self._unitCombo.setFixedHeight(25)
self._UnitCombo.addItem(display, data)
self._UnitCombo.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addWidget(self._spinBox)
layout.addWidget(self._unitCombo)
layout.addStretch()
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
Layout.setSpacing(4)
Layout.addWidget(self._SpinBox)
Layout.addWidget(self._UnitCombo)
Layout.addStretch()
def getValue(
self
@@ -201,8 +201,8 @@ class _DateOffsetContainer(QWidget):
self
) -> int:
val = self._spinBox.value()
unit = self._unitCombo.currentData()
val = self._SpinBox.value()
unit = self._UnitCombo.currentData()
if unit == "weeks":
return val*7
if unit == "months":
@@ -220,14 +220,14 @@ class _TimeOffsetContainer(QWidget):
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setSuffix(" 小时")
self._spinBox.setFixedHeight(25)
self._SpinBox = QSpinBox(self)
self._SpinBox.setRange(0, 99999)
self._SpinBox.setSuffix(" 小时")
self._SpinBox.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._spinBox)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(0, 0, 0, 0)
Layout.addWidget(self._SpinBox)
def getValue(
self
@@ -239,7 +239,7 @@ class _TimeOffsetContainer(QWidget):
self
) -> int:
return self._spinBox.value()
return self._SpinBox.value()
class VariableManager(QObject):
@@ -364,11 +364,11 @@ def makeVarRefCombo(
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
cb.setFixedHeight(25)
cb.setMinimumWidth(120)
cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return cb
Cb = QComboBox(parent)
Cb.setFixedHeight(25)
Cb.setMinimumWidth(120)
Cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return Cb
def makeComboWidget(
items,
@@ -376,12 +376,12 @@ def makeComboWidget(
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
Cb = QComboBox(parent)
for display, data in items:
cb.addItem(display, data)
cb.setFixedHeight(25)
cb.setMinimumWidth(min_width)
return cb
Cb.addItem(display, data)
Cb.setFixedHeight(25)
Cb.setMinimumWidth(min_width)
return Cb
def makeLabel(
text: str,
@@ -389,11 +389,11 @@ def makeLabel(
width: int = None
) -> QLabel:
lbl = QLabel(text, parent)
lbl.setFixedHeight(25)
Lbl = QLabel(text, parent)
Lbl.setFixedHeight(25)
if width:
lbl.setFixedWidth(width)
return lbl
Lbl.setFixedWidth(width)
return Lbl
def getValueFromWidget(
w: QWidget
+124 -124
View File
@@ -66,42 +66,42 @@ class ConditionRowFrame(QFrame):
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(32)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(2, 2, 2, 2)
Layout.setSpacing(4)
if self._isFirst:
self.logicCombo = None
self.LogicCombo = None
else:
self.logicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
layout.addWidget(self.logicCombo)
self.leftVarCombo = QComboBox(self)
self.leftVarCombo.setFixedHeight(25)
self.leftVarCombo.setMinimumWidth(120)
self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
Layout.addWidget(self.LogicCombo)
self.LeftVarCombo = QComboBox(self)
self.LeftVarCombo.setFixedHeight(25)
self.LeftVarCombo.setMinimumWidth(120)
self.LeftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.populateLeftVarCombo()
layout.addWidget(self.leftVarCombo)
self.opCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
layout.addWidget(self.opCombo)
self._compTypeCombo = makeComboWidget([
Layout.addWidget(self.LeftVarCombo)
self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
Layout.addWidget(self.OpCombo)
self._CompTypeCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self._compTypeCombo)
self.rhsStack = QStackedWidget(self)
self.rhsStack.setFixedHeight(25)
Layout.addWidget(self._CompTypeCombo)
self.RhsStack = QStackedWidget(self)
self.RhsStack.setFixedHeight(25)
self.initLiteralStack()
self.rhsVarCombo = makeVarRefCombo(self)
self.rhsStack.addWidget(self.rhsVarCombo)
self.rhsStack.setCurrentIndex(0)
layout.addWidget(self.rhsStack)
self.RhsVarCombo = makeVarRefCombo(self)
self.RhsStack.addWidget(self.RhsVarCombo)
self.RhsStack.setCurrentIndex(0)
Layout.addWidget(self.RhsStack)
if not self._isFirst:
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
self.DeleteBtn = QPushButton("×", self)
self.DeleteBtn.setFixedSize(25, 25)
self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
Layout.addWidget(self.DeleteBtn)
else:
self.deleteBtn = None
layout.addStretch()
self.DeleteBtn = None
Layout.addStretch()
self.setUpdatesEnabled(True)
def populateLeftVarCombo(
@@ -111,53 +111,53 @@ class ConditionRowFrame(QFrame):
wasBool = self._isBoolMode
boolName = None
if wasBool:
data = self.leftVarCombo.currentData()
data = self.LeftVarCombo.currentData()
if data:
boolName = data[0]
self._varMgr.populateCombo(self.leftVarCombo)
self._varMgr.populateCombo(self.LeftVarCombo)
# Append boolean literal sentinels at the end
self.leftVarCombo.insertSeparator(self.leftVarCombo.count())
self.leftVarCombo.addItem("true", ("true", "Boolean"))
self.leftVarCombo.addItem("false", ("false", "Boolean"))
self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count())
self.LeftVarCombo.addItem("true", ("true", "Boolean"))
self.LeftVarCombo.addItem("false", ("false", "Boolean"))
if wasBool and boolName:
for ci in range(self.leftVarCombo.count()):
d = self.leftVarCombo.itemData(ci)
for ci in range(self.LeftVarCombo.count()):
d = self.LeftVarCombo.itemData(ci)
if d and d[0] == boolName:
self.leftVarCombo.setCurrentIndex(ci)
self.LeftVarCombo.setCurrentIndex(ci)
break
def populateRHSVarCombo(
self
):
self._varMgr.populateCombo(self.rhsVarCombo)
self._varMgr.populateCombo(self.RhsVarCombo)
def initLiteralStack(
self
):
self.literalStack = QStackedWidget(self)
self.literalStack.setFixedHeight(25)
self.LiteralStack = QStackedWidget(self)
self.LiteralStack.setFixedHeight(25)
self._literalWidgets = {}
for vt in getTypeOrder():
w = makeValueWidget(vt, self.literalStack)
self._literalWidgets[vt] = w
self.literalStack.addWidget(w)
self.literalStack.setCurrentWidget(self._literalWidgets.get("String"))
self.rhsStack.addWidget(self.literalStack)
W = makeValueWidget(vt, self.LiteralStack)
self._literalWidgets[vt] = W
self.LiteralStack.addWidget(W)
self.LiteralStack.setCurrentWidget(self._literalWidgets.get("String"))
self.RhsStack.addWidget(self.LiteralStack)
def connectSignals(
self
):
self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
def getLogic(
self
) -> str:
return self.logicCombo.currentData() if self.logicCombo else ""
return self.LogicCombo.currentData() if self.LogicCombo else ""
def updateRHSLiteralWidget(
self,
@@ -166,13 +166,13 @@ class ConditionRowFrame(QFrame):
if vartype not in self._literalWidgets:
vartype = "String"
self.literalStack.setCurrentWidget(self._literalWidgets[vartype])
self.LiteralStack.setCurrentWidget(self._literalWidgets[vartype])
def toScript(
self
) -> str:
data = self.leftVarCombo.currentData()
data = self.LeftVarCombo.currentData()
if self._isBoolMode and data:
return data[0]
if not data:
@@ -183,12 +183,12 @@ class ConditionRowFrame(QFrame):
name = "datenow()"
elif name == "CURRENT_TIME":
name = "timenow()"
opSym = self.opCombo.currentData()
opSym = self.OpCombo.currentData()
if self._rawRhsExpr:
return f"{name} {opSym} {self._rawRhsExpr}"
isVarRef = (self._compTypeCombo.currentData() == "variable")
isVarRef = (self._CompTypeCombo.currentData() == "variable")
if isVarRef:
rd = self.rhsVarCombo.currentData()
rd = self.RhsVarCombo.currentData()
if rd:
rhsName = rd[0]
if rhsName == "CURRENT_DATE":
@@ -196,7 +196,7 @@ class ConditionRowFrame(QFrame):
elif rhsName == "CURRENT_TIME":
rhsName = "timenow()"
return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip()
rhsText = self.RhsVarCombo.currentText().strip()
if rhsText:
return f"{name} {opSym} {rhsText}"
return ""
@@ -223,15 +223,15 @@ class ConditionRowFrame(QFrame):
self._rawRhsExpr = ""
if idx < 0:
return
data = self.leftVarCombo.itemData(idx)
data = self.LeftVarCombo.itemData(idx)
if not data:
return
name, vartype = data
isBool = name in ("true", "false")
self._isBoolMode = isBool
self.opCombo.setVisible(not isBool)
self._compTypeCombo.setVisible(not isBool)
self.rhsStack.setVisible(not isBool)
self.OpCombo.setVisible(not isBool)
self._CompTypeCombo.setVisible(not isBool)
self.RhsStack.setVisible(not isBool)
if not isBool:
self.updateRHSLiteralWidget(vartype)
@@ -242,8 +242,8 @@ class ConditionRowFrame(QFrame):
):
self._rawRhsExpr = ""
isVar = (self._compTypeCombo.currentData() == "variable")
self.rhsStack.setCurrentIndex(1 if isVar else 0)
isVar = (self._CompTypeCombo.currentData() == "variable")
self.RhsStack.setCurrentIndex(1 if isVar else 0)
if isVar:
self.populateRHSVarCombo()
@@ -273,52 +273,52 @@ class ActionStepFrame(QFrame):
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(35)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
self.opTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self)
layout.addWidget(self.opTypeCombo)
layout.addWidget(makeLabel("设置", self))
self.targetCombo = QComboBox(self)
self.targetCombo.setFixedHeight(25)
self.targetCombo.setMinimumWidth(120)
Layout = QHBoxLayout(self)
Layout.setContentsMargins(2, 2, 2, 2)
Layout.setSpacing(4)
self.OpTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self)
Layout.addWidget(self.OpTypeCombo)
Layout.addWidget(makeLabel("设置", self))
self.TargetCombo = QComboBox(self)
self.TargetCombo.setFixedHeight(25)
self.TargetCombo.setMinimumWidth(120)
self.populateTargetCombo()
layout.addWidget(self.targetCombo)
layout.addWidget(makeLabel("", self))
self.valueSrcCombo = makeComboWidget([
Layout.addWidget(self.TargetCombo)
Layout.addWidget(makeLabel("", self))
self.ValueSrcCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self.valueSrcCombo)
self.valueStack = QStackedWidget(self)
self.valueStack.setFixedHeight(25)
Layout.addWidget(self.ValueSrcCombo)
self.ValueStack = QStackedWidget(self)
self.ValueStack.setFixedHeight(25)
self.initValueStacks()
layout.addWidget(self.valueStack)
self.existingVarCombo = makeVarRefCombo(self)
self.existingVarCombo.setVisible(False)
layout.addWidget(self.existingVarCombo)
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
Layout.addWidget(self.ValueStack)
self.ExistingVarCombo = makeVarRefCombo(self)
self.ExistingVarCombo.setVisible(False)
Layout.addWidget(self.ExistingVarCombo)
self.DeleteBtn = QPushButton("×", self)
self.DeleteBtn.setFixedSize(25, 25)
self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
Layout.addWidget(self.DeleteBtn)
self.setUpdatesEnabled(True)
def populateTargetCombo(
self
):
self.targetCombo.blockSignals(True)
self.targetCombo.clear()
self.TargetCombo.blockSignals(True)
self.TargetCombo.clear()
for p in getPresetVars():
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue
info = self._varMgr.getInfoByName(p["name"])
if info:
self.targetCombo.addItem(
self.TargetCombo.addItem(
info["display"],
(info["name"], info["type"])
)
self.targetCombo.blockSignals(False)
self.TargetCombo.blockSignals(False)
def initValueStacks(
self
@@ -327,45 +327,45 @@ class ActionStepFrame(QFrame):
self._literalWidgets = {}
self._offsetWidgets = {}
for vt in getTypeOrder():
self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack)
self.valueStack.addWidget(self._literalWidgets[vt])
self._literalWidgets[vt] = makeValueWidget(vt, self.ValueStack)
self.ValueStack.addWidget(self._literalWidgets[vt])
if getArithType(vt):
self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack)
self.valueStack.addWidget(self._offsetWidgets[vt])
self._offsetWidgets[vt] = makeOffsetWidget(vt, self.ValueStack)
self.ValueStack.addWidget(self._offsetWidgets[vt])
else:
lbl = QLabel("(不支持该操作)", self.valueStack)
lbl.setFixedHeight(25)
self._offsetWidgets[vt] = lbl
self.valueStack.addWidget(lbl)
Lbl = QLabel("(不支持该操作)", self.ValueStack)
Lbl.setFixedHeight(25)
self._offsetWidgets[vt] = Lbl
self.ValueStack.addWidget(Lbl)
def connectSignals(
self
):
self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
self.valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
self.OpTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
self.TargetCombo.currentIndexChanged.connect(self.onTargetChanged)
self.ValueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
def getTargetName(
self
) -> str:
data = self.targetCombo.currentData()
data = self.TargetCombo.currentData()
return data[0] if data else ""
def updateValueWidget(
self
):
op = self.opTypeCombo.currentData()
op = self.OpTypeCombo.currentData()
isArith = (op in ("add", "sub"))
actualType = self._currentTargetType
if isArith and actualType in self._offsetWidgets:
self.valueStack.setCurrentWidget(self._offsetWidgets[actualType])
self.ValueStack.setCurrentWidget(self._offsetWidgets[actualType])
elif actualType in self._literalWidgets:
self.valueStack.setCurrentWidget(self._literalWidgets[actualType])
self.ValueStack.setCurrentWidget(self._literalWidgets[actualType])
else:
self.valueStack.setCurrentWidget(self._literalWidgets.get("String"))
self.ValueStack.setCurrentWidget(self._literalWidgets.get("String"))
def toScript(
self
@@ -375,7 +375,7 @@ class ActionStepFrame(QFrame):
"""
target = self.getTargetName()
op = self.opTypeCombo.currentData()
op = self.OpTypeCombo.currentData()
if op == "pass":
return " -- pass"
if not target:
@@ -386,19 +386,19 @@ class ActionStepFrame(QFrame):
encoded = encodeValueStr(rawVal, vartype)
return f" {target} = {encoded}"
elif op == "add":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays()
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
days = self.ValueStack.currentWidget().getOffsetDays()
return f" {target} = dateadd({target}, {days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours()
if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, {hours})"
return f" {target} = {target} + {rawVal}"
elif op == "sub":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays()
if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
days = self.ValueStack.currentWidget().getOffsetDays()
return f" {target} = dateadd({target}, -{days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours()
if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, -{hours})"
return f" {target} = {target} - {rawVal}"
return ""
@@ -407,10 +407,10 @@ class ActionStepFrame(QFrame):
self
) -> str:
if self.valueSrcCombo.currentData() == "variable":
data = self.existingVarCombo.currentData()
if self.ValueSrcCombo.currentData() == "variable":
data = self.ExistingVarCombo.currentData()
return data[0] if data else ""
w = self.valueStack.currentWidget()
w = self.ValueStack.currentWidget()
if w:
return getValueFromWidget(w)
return ""
@@ -419,15 +419,15 @@ class ActionStepFrame(QFrame):
self
):
currentData = self.targetCombo.currentData()
currentData = self.TargetCombo.currentData()
self.populateTargetCombo()
if currentData:
for i in range(self.targetCombo.count()):
d = self.targetCombo.itemData(i)
for i in range(self.TargetCombo.count()):
d = self.TargetCombo.itemData(i)
if d and d[0] == currentData[0]:
self.targetCombo.setCurrentIndex(i)
self.TargetCombo.setCurrentIndex(i)
break
self._varMgr.populateCombo(self.existingVarCombo)
self._varMgr.populateCombo(self.ExistingVarCombo)
@Slot(int)
def onTargetChanged(
@@ -437,13 +437,13 @@ class ActionStepFrame(QFrame):
if idx < 0:
return
data = self.targetCombo.itemData(idx)
data = self.TargetCombo.itemData(idx)
if not data:
return
_, vartype = data
self._currentTargetType = vartype
self.updateValueWidget()
self.onValueSrcChanged(self.valueSrcCombo.currentIndex())
self.onValueSrcChanged(self.ValueSrcCombo.currentIndex())
@Slot(int)
def onOpTypeChanged(
@@ -459,10 +459,10 @@ class ActionStepFrame(QFrame):
idx
):
isVar = (self.valueSrcCombo.currentData() == "variable")
self.valueStack.setVisible(not isVar)
self.existingVarCombo.setVisible(isVar)
isVar = (self.ValueSrcCombo.currentData() == "variable")
self.ValueStack.setVisible(not isVar)
self.ExistingVarCombo.setVisible(isVar)
if isVar:
self._varMgr.populateCombo(self.existingVarCombo)
self._varMgr.populateCombo(self.ExistingVarCombo)
else:
self.updateValueWidget()
+100 -100
View File
@@ -386,18 +386,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config = self.defaultUserConfig()
for i in range(self.UserTreeWidget.topLevelItemCount()):
group_item = self.UserTreeWidget.topLevelItem(i)
GroupItem = self.UserTreeWidget.topLevelItem(i)
group_config = {
"name": group_item.text(0),
"enabled": group_item.checkState(1) == Qt.CheckState.Checked,
"name": GroupItem.text(0),
"enabled": GroupItem.checkState(1) == Qt.CheckState.Checked,
"users": []
}
for j in range(group_item.childCount()):
user_item = group_item.child(j)
user = user_item.data(0, Qt.UserRole)
for j in range(GroupItem.childCount()):
UserItem = GroupItem.child(j)
user = UserItem.data(0, Qt.UserRole)
if not user:
continue
user["enabled"] = user_item.checkState(1) == Qt.CheckState.Checked
user["enabled"] = UserItem.checkState(1) == Qt.CheckState.Checked
group_config["users"].append(user)
user_config["groups"].append(group_config)
return user_config
@@ -453,18 +453,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
try:
if "groups" in users:
for group_config in users["groups"]:
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
group_item.setText(0, group_config["name"])
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
GroupItem.setText(0, group_config["name"])
GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable)
GroupItem.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
for user_config in group_config["users"]:
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
user_item.setText(0, user_config["username"])
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
user_item.setData(0, Qt.UserRole, user_config)
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
user_item.setDisabled(not group_config.get("enabled", True))
group_item.setExpanded(True)
UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value)
UserItem.setText(0, user_config["username"])
UserItem.setText(1, "" if user_config.get("enabled", True) else "跳过")
UserItem.setData(0, Qt.UserRole, user_config)
UserItem.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
UserItem.setDisabled(not group_config.get("enabled", True))
GroupItem.setExpanded(True)
except KeyError as e:
QMessageBox.warning(
self,
@@ -638,43 +638,43 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> QTreeWidgetItem:
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
if not group_name:
group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}"
group_item.setText(0, group_name)
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked)
self.UserTreeWidget.setCurrentItem(group_item)
GroupItem.setText(0, group_name)
GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable)
GroupItem.setCheckState(1, Qt.Checked)
self.UserTreeWidget.setCurrentItem(GroupItem)
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return group_item
return GroupItem
def delGroup(
self,
group_item: QTreeWidgetItem = None
GroupItem: QTreeWidgetItem = None
):
if group_item is None:
if GroupItem is None:
return
if group_item.type() != ALUserTreeItemType.GROUP.value:
if GroupItem.type() != ALUserTreeItemType.GROUP.value:
return
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
index = self.UserTreeWidget.indexOfTopLevelItem(GroupItem)
self.UserTreeWidget.takeTopLevelItem(index)
def addUser(
self,
group_item: QTreeWidgetItem = None
GroupItem: QTreeWidgetItem = None
) -> QTreeWidgetItem:
if group_item is None:
current_item = self.UserTreeWidget.currentItem()
if current_item is None:
group_item = self.addGroup()
if group_item.type() == ALUserTreeItemType.USER.value:
group_item = group_item.parent()
if group_item.checkState(1) == Qt.CheckState.Unchecked:
if GroupItem is None:
CurrentItem = self.UserTreeWidget.currentItem()
if CurrentItem is None:
GroupItem = self.addGroup()
if GroupItem.type() == ALUserTreeItemType.USER.value:
GroupItem = GroupItem.parent()
if GroupItem.checkState(1) == Qt.CheckState.Unchecked:
return None
new_user = {
"username": f"新用户-{group_item.childCount()}",
"username": f"新用户-{GroupItem.childCount()}",
"password": "000000",
"enabled": True,
"reserve_info": {
@@ -703,30 +703,30 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
}
}
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
user_item.setText(0, new_user["username"])
user_item.setText(1, "")
user_item.setData(0, Qt.UserRole, new_user)
user_item.setCheckState(1, Qt.CheckState.Checked)
group_item.setExpanded(True)
self.UserTreeWidget.setCurrentItem(user_item)
UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value)
UserItem.setText(0, new_user["username"])
UserItem.setText(1, "")
UserItem.setData(0, Qt.UserRole, new_user)
UserItem.setCheckState(1, Qt.CheckState.Checked)
GroupItem.setExpanded(True)
self.UserTreeWidget.setCurrentItem(UserItem)
self.setUserToWidget(new_user)
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return user_item
return UserItem
def delUser(
self,
user_item: QTreeWidgetItem = None
UserItem: QTreeWidgetItem = None
):
if user_item is None:
if UserItem is None:
return
if user_item.type() != ALUserTreeItemType.USER.value:
if UserItem.type() != ALUserTreeItemType.USER.value:
return
parent_item = user_item.parent()
index = parent_item.indexOfChild(user_item)
parent_item.takeChild(index)
if parent_item.childCount() == 0:
ParentItem = UserItem.parent()
index = ParentItem.indexOfChild(UserItem)
ParentItem.takeChild(index)
if ParentItem.childCount() == 0:
self.UserTreeWidget.setCurrentItem(None)
def renameItem(
@@ -787,19 +787,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
room = self.RoomComboBox.currentText()
floor_idx = self.__floor_rmap[floor]
room_idx = self.__room_rmap[room]
dialog = ALSeatMapSelectDialog(
Dialog = ALSeatMapSelectDialog(
self,
floor,
room,
ALSeatMapTable[floor_idx][room_idx]
)
dialog.selectSeats(self.SeatIDEdit.text().split(","))
if dialog.exec() == QDialog.DialogCode.Accepted:
selected_seats = dialog.getSelectedSeats()
Dialog.selectSeats(self.SeatIDEdit.text().split(","))
if Dialog.exec() == QDialog.DialogCode.Accepted:
selected_seats = Dialog.getSelectedSeats()
if len(selected_seats) == 0:
self.SeatIDEdit.clear()
return
self.SeatIDEdit.setText(",".join(dialog.getSelectedSeats()))
self.SeatIDEdit.setText(",".join(Dialog.getSelectedSeats()))
@Slot()
def onUserTreeWidgetCurrentItemChanged(
@@ -844,10 +844,10 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if item.type() == ALUserTreeItemType.GROUP.value:
is_checked = item.checkState(1) == Qt.CheckState.Checked
for i in range(item.childCount()):
child = item.child(i)
if self.UserTreeWidget.currentItem() == child:
Child = item.child(i)
if self.UserTreeWidget.currentItem() == Child:
self.UserTreeWidget.setCurrentItem(item)
child.setDisabled(not is_checked)
Child.setDisabled(not is_checked)
else:
is_checked = item.checkState(1) == Qt.CheckState.Checked
item.setText(1, "" if is_checked else "跳过")
@@ -857,41 +857,41 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
menu: QMenu
):
add_group_action = QAction("添加分组", menu)
add_group_action.triggered.connect(self.addGroup)
menu.addAction(add_group_action)
AddGroupAction = QAction("添加分组", menu)
AddGroupAction.triggered.connect(self.addGroup)
menu.addAction(AddGroupAction)
def showGroupMenu(
self,
menu: QMenu,
group_item: QTreeWidgetItem = None
GroupItem: QTreeWidgetItem = None
):
add_user_action = QAction("添加用户", menu)
rename_group_action = QAction("重命名分组", menu)
del_group_action = QAction("删除分组", menu)
add_user_action.triggered.connect(lambda: self.addUser(group_item))
rename_group_action.triggered.connect(lambda: self.renameItem(group_item))
del_group_action.triggered.connect(lambda: self.delGroup(group_item))
menu.addAction(add_user_action)
AddUserAction = QAction("添加用户", menu)
RenameGroupAction = QAction("重命名分组", menu)
DelGroupAction = QAction("删除分组", menu)
AddUserAction.triggered.connect(lambda: self.addUser(GroupItem))
RenameGroupAction.triggered.connect(lambda: self.renameItem(GroupItem))
DelGroupAction.triggered.connect(lambda: self.delGroup(GroupItem))
menu.addAction(AddUserAction)
menu.addSeparator()
menu.addAction(rename_group_action)
menu.addAction(del_group_action)
if group_item.checkState(1) == Qt.CheckState.Unchecked:
add_user_action.setEnabled(False)
menu.addAction(RenameGroupAction)
menu.addAction(DelGroupAction)
if GroupItem.checkState(1) == Qt.CheckState.Unchecked:
AddUserAction.setEnabled(False)
def showUserMenu(
self,
menu: QMenu,
user_item: QTreeWidgetItem = None
UserItem: QTreeWidgetItem = None
):
rename_user_action = QAction("重命名用户", menu)
del_user_action = QAction("删除用户", menu)
rename_user_action.triggered.connect(lambda: self.renameItem(user_item))
del_user_action.triggered.connect(lambda: self.delUser(user_item))
menu.addAction(rename_user_action)
menu.addAction(del_user_action)
RenameUserAction = QAction("重命名用户", menu)
DelUserAction = QAction("删除用户", menu)
RenameUserAction.triggered.connect(lambda: self.renameItem(UserItem))
DelUserAction.triggered.connect(lambda: self.delUser(UserItem))
menu.addAction(RenameUserAction)
menu.addAction(DelUserAction)
@Slot()
def onUserTreeWidgetContextMenu(
@@ -899,31 +899,31 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
pos
):
current_item = self.UserTreeWidget.itemAt(pos)
menu = QMenu(self.UserTreeWidget)
if current_item is None:
self.showTreeMenu(menu)
elif current_item.type() == ALUserTreeItemType.GROUP.value:
self.showGroupMenu(menu, current_item)
CurrentItem = self.UserTreeWidget.itemAt(pos)
Menu = QMenu(self.UserTreeWidget)
if CurrentItem is None:
self.showTreeMenu(Menu)
elif CurrentItem.type() == ALUserTreeItemType.GROUP.value:
self.showGroupMenu(Menu, CurrentItem)
else:
self.showUserMenu(menu, current_item)
menu.exec_(self.UserTreeWidget.mapToGlobal(pos))
self.showUserMenu(Menu, CurrentItem)
Menu.exec_(self.UserTreeWidget.mapToGlobal(pos))
@Slot()
def onAddUserButtonClicked(
self
):
current_item = self.UserTreeWidget.currentItem()
self.addUser(current_item)
CurrentItem = self.UserTreeWidget.currentItem()
self.addUser(CurrentItem)
@Slot()
def onDelUserButtonClicked(
self
):
current_item = self.UserTreeWidget.currentItem()
self.delUser(current_item)
CurrentItem = self.UserTreeWidget.currentItem()
self.delUser(CurrentItem)
@Slot()
def onBrowseBrowserDriverButtonClicked(
@@ -944,10 +944,10 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self
):
dialog = ALWebDriverDownloadDialog(self)
dialog.show()
dialog.exec_()
selected_driver_info = dialog.getSelectedDriverInfo()
Dialog = ALWebDriverDownloadDialog(self)
Dialog.show()
Dialog.exec_()
selected_driver_info = Dialog.getSelectedDriverInfo()
if selected_driver_info and selected_driver_info.driver_path:
self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
@@ -1133,8 +1133,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self
):
current_item = self.UserTreeWidget.currentItem()
if current_item and current_item.type() == ALUserTreeItemType.USER.value:
CurrentItem = self.UserTreeWidget.currentItem()
if CurrentItem and CurrentItem.type() == ALUserTreeItemType.USER.value:
self.UserTreeWidget.setCurrentItem(None)
if self.saveConfigs(
self.__config_paths["run"],
+7 -7
View File
@@ -76,8 +76,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
self.icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg")
self.setWindowIcon(self.icon)
self.Icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg")
self.setWindowIcon(self.Icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
@@ -106,15 +106,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self
):
about_dialog = ALAboutDialog(self)
about_dialog.exec()
AboutDialog = ALAboutDialog(self)
AboutDialog.exec()
def onManualActionTriggered(
self
):
url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(url)
Url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(Url)
def setupTray(
self
@@ -123,7 +123,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if not QSystemTrayIcon.isSystemTrayAvailable():
self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING)
return
self.TrayIcon = QSystemTrayIcon(self.icon, self)
self.TrayIcon = QSystemTrayIcon(self.Icon, self)
self.TrayIcon.setToolTip("AutoLibrary")
self.TrayMenu = QMenu()
+8 -8
View File
@@ -103,15 +103,15 @@ class ALSeatMapView(QGraphicsView):
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
SeatWidget = ALSeatFrame(seat_number)
SeatWidget.clicked.connect(self.onSeatClicked)
self.SeatsContainerLayout.addWidget(SeatWidget, row_idx, col_idx)
self.__seat_frames[seat_number] = SeatWidget
else:
spacer = QFrame()
spacer.setFixedSize(20, 30)
spacer.setStyleSheet("background-color: transparent; border: none;")
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
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)
+67 -68
View File
@@ -56,7 +56,6 @@ class ALStatusLabel(QLabel):
self.setFixedSize(36, 36)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.RunningAnimation = QPropertyAnimation(self, b"iconAngle")
self.RunningAnimation.setDuration(1000)
self.RunningAnimation.setStartValue(0)
@@ -119,35 +118,35 @@ class ALStatusLabel(QLabel):
event
):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
Painter = QPainter(self)
Painter.setRenderHint(QPainter.RenderHint.Antialiasing)
center_x = self.width()/2
center_y = self.height()/2
radius = min(center_x, center_y) - 3
match self.__status:
case self.Status.WAITING:
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#969696")) # grey
painter.setPen(pen)
painter.drawEllipse(
Pen = Painter.pen()
Pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setColor(QColor("#969696")) # grey
Painter.setPen(Pen)
Painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
case self.Status.RUNNING:
gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
gradient.setColorAt(1.0, QColor("#2294FF00"))
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(gradient)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(pen)
painter.drawEllipse(
Gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
Gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
Gradient.setColorAt(1.0, QColor("#2294FF00"))
Pen = Painter.pen()
Pen.setWidth(3)
Pen.setBrush(Gradient)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Painter.setPen(Pen)
Painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
@@ -155,102 +154,102 @@ class ALStatusLabel(QLabel):
)
case self.Status.SUCCESS:
# draw the success green circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
painter.setPen(pen)
painter.drawEllipse(
Pen = Painter.pen()
Pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
Painter.setPen(Pen)
Painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the success check mark '✓'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Painter.setPen(Qt.PenStyle.SolidLine)
Pen = Painter.pen()
Pen.setWidth(3)
Pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
Pen.setColor(self.getMarkColor())
Painter.setPen(Pen)
mark_size = radius/2
mark_path = [
(center_x - mark_size, center_y),
(center_x - mark_size/3, center_y + mark_size/2),
(center_x + mark_size, center_y - mark_size/2)
]
painter.drawLine(
Painter.drawLine(
int(mark_path[0][0]),int(mark_path[0][1]),
int(mark_path[1][0]),int(mark_path[1][1])
)
painter.drawLine(
Painter.drawLine(
int(mark_path[1][0]),int(mark_path[1][1]),
int(mark_path[2][0]),int(mark_path[2][1])
)
case self.Status.WARNING:
# draw the warning orange circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#FF9800")) # orange
painter.setPen(pen)
painter.drawEllipse(
Pen = Painter.pen()
Pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setColor(QColor("#FF9800")) # orange
Painter.setPen(Pen)
Painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the warning exclamation mark '!'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Painter.setPen(Qt.PenStyle.SolidLine)
Pen = Painter.pen()
Pen.setWidth(3)
Pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
painter.drawLine(
Pen.setColor(self.getMarkColor())
Painter.setPen(Pen)
Painter.drawLine(
int(center_x), int(center_y - radius/2),
int(center_x), int(center_y + radius/6)
)
painter.drawPoint(
Painter.drawPoint(
int(center_x), int(center_y + radius/2)
)
case self.Status.FAILURE:
# draw the failure red circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#DC0000")) # red
painter.setPen(pen)
painter.drawEllipse(
Pen = Painter.pen()
Pen.setWidth(2)
Pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Pen.setColor(QColor("#DC0000")) # red
Painter.setPen(Pen)
Painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the failure cross mark '✗'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
Painter.setPen(Qt.PenStyle.SolidLine)
Pen = Painter.pen()
Pen.setWidth(3)
Pen.setBrush(Qt.BrushStyle.NoBrush)
Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
Pen.setColor(self.getMarkColor())
Painter.setPen(Pen)
mark_size = radius/3
painter.drawLine(
Painter.drawLine(
int(center_x - mark_size), int(center_y - mark_size),
int(center_x + mark_size), int(center_y + mark_size)
)
painter.drawLine(
Painter.drawLine(
int(center_x + mark_size), int(center_y - mark_size),
int(center_x - mark_size), int(center_y + mark_size)
)
painter.end()
Painter.end()
super().paintEvent(event)
+11 -13
View File
@@ -80,7 +80,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0)
@@ -108,17 +107,16 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox)
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False)
self.AutoScriptGroupBox = QGroupBox("AutoScript 指令")
self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox)
self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3)
self.AutoScriptLayout.setSpacing(3)
autoScriptBtnLayout = QHBoxLayout()
AutoScriptBtnLayout = QHBoxLayout()
self.AutoScriptEditButton = QPushButton("编辑")
self.AutoScriptEditButton.setMinimumHeight(25)
self.AutoScriptEditButton.setFixedWidth(80)
autoScriptBtnLayout.addWidget(self.AutoScriptEditButton)
autoScriptBtnLayout.addStretch()
AutoScriptBtnLayout.addWidget(self.AutoScriptEditButton)
AutoScriptBtnLayout.addStretch()
self.AutoScriptHelpButton = QPushButton("?")
self.AutoScriptHelpButton.setFixedSize(20, 20)
self.AutoScriptHelpButton.setToolTip(
@@ -132,12 +130,12 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
"font-weight: bold; color: #555; }"
"QPushButton:hover { background-color: #E0E0E0; }"
)
autoScriptBtnLayout.addWidget(self.AutoScriptHelpButton)
AutoScriptBtnLayout.addWidget(self.AutoScriptHelpButton)
self.AutoScriptStatusLabel = QLabel("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
self.AutoScriptStatusLabel.setFixedHeight(25)
autoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel)
self.AutoScriptLayout.addLayout(autoScriptBtnLayout)
AutoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel)
self.AutoScriptLayout.addLayout(AutoScriptBtnLayout)
self.ALAddTimerTaskLayout.insertWidget(
self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1,
self.AutoScriptGroupBox
@@ -305,18 +303,18 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
@Slot()
def onPreviewAutoScript(self):
from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog
dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data)
if dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript()
Dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data)
if Dlg.exec() == QDialog.DialogCode.Accepted:
script = Dlg.getScript()
self.__auto_script = script
self.__mock_target_data = dlg.getMockData()
self.__mock_target_data = Dlg.getMockData()
if script:
self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
else:
self.AutoScriptStatusLabel.setText("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
dlg.deleteLater()
Dlg.deleteLater()
@Slot()
def onAutoScriptHelp(
-3
View File
@@ -41,7 +41,6 @@ class ALTimerTaskHistoryDialog(QDialog):
self.setWindowTitle("定时任务执行历史 - AutoLibrary")
self.setMinimumSize(300, 300)
self.setMaximumSize(500, 400)
MainLayout = QVBoxLayout(self)
InfoLayout = QGridLayout()
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
@@ -51,7 +50,6 @@ class ALTimerTaskHistoryDialog(QDialog):
TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;")
InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
InfoLayout.setColumnStretch(0, 1)
if self.__task_data.get("repeat", False):
RepeatLabel = QLabel("可重复性任务")
RepeatLabel.setStyleSheet("color: #2294FF; font-size: 12px;")
@@ -68,7 +66,6 @@ class ALTimerTaskHistoryDialog(QDialog):
self.HistoryTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.loadHistory()
MainLayout.addWidget(self.HistoryTableWidget)
ButtonLayout = QHBoxLayout()
ButtonLayout.addStretch()
self.CloseButton = QPushButton("关闭")
+44 -44
View File
@@ -173,20 +173,20 @@ class ALTimerTaskItemWidget(QWidget):
pos
):
menu = QMenu(self)
edit_action = QAction("编辑", self)
edit_action.triggered.connect(
Menu = QMenu(self)
EditAction = QAction("编辑", self)
EditAction.triggered.connect(
lambda: self.editRequested.emit(self.__timer_task)
)
menu.addAction(edit_action)
Menu.addAction(EditAction)
if self.__timer_task["status"] != ALTimerTaskStatus.RUNNING\
and self.__timer_task["status"] != ALTimerTaskStatus.READY:
delete_action = QAction("删除", self)
delete_action.triggered.connect(
DeleteAction = QAction("删除", self)
DeleteAction.triggered.connect(
lambda: self.__manage_widget.deleteTask(self.__timer_task)
)
menu.addAction(delete_action)
menu.exec(self.mapToGlobal(pos))
Menu.addAction(DeleteAction)
Menu.exec(self.mapToGlobal(pos))
class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
@@ -209,7 +209,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
super().__init__(parent)
self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
self.__timer_tasks = []
self.__check_timer = None
self.__CheckTimer = None
self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
self.__sort_order = Qt.SortOrder.AscendingOrder
@@ -233,9 +233,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self
):
self.__check_timer = QTimer(self)
self.__check_timer.timeout.connect(self.checkTasks)
self.__check_timer.start(500)
self.__CheckTimer = QTimer(self)
self.__CheckTimer.timeout.connect(self.checkTasks)
self.__CheckTimer.start(500)
def initializeTimerTasks(
self
@@ -386,28 +386,28 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.TimerTasksListWidget.clear()
self.sortTimerTasks(self.__sort_policy, self.__sort_order)
for timer_task in self.__timer_tasks:
item = QListWidgetItem()
item.setData(Qt.UserRole, timer_task)
widget = ALTimerTaskItemWidget(self, timer_task)
widget.DeleteButton.clicked.connect(
Item = QListWidgetItem()
Item.setData(Qt.UserRole, timer_task)
Widget = ALTimerTaskItemWidget(self, timer_task)
Widget.DeleteButton.clicked.connect(
lambda _, task = timer_task: self.deleteTask(task)
)
if timer_task.get("repeat", False) and hasattr(widget, "HistoryButton"):
widget.HistoryButton.clicked.connect(
if timer_task.get("repeat", False) and hasattr(Widget, "HistoryButton"):
Widget.HistoryButton.clicked.connect(
lambda _, task = timer_task: self.showTaskHistory(task)
)
widget.editRequested.connect(self.editTask)
item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget)
Widget.editRequested.connect(self.editTask)
Item.setSizeHint(Widget.size())
self.TimerTasksListWidget.addItem(Item)
self.TimerTasksListWidget.setItemWidget(Item, Widget)
def addTask(
self
):
dialog = ALTimerTaskAddDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = dialog.getTimerTask()
Dialog = ALTimerTaskAddDialog(self)
if Dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = Dialog.getTimerTask()
self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit()
@@ -416,9 +416,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_task: dict
):
dialog = ALTimerTaskAddDialog(self, timer_task)
if dialog.exec() == QDialog.DialogCode.Accepted:
updated = dialog.getTimerTask()
Dialog = ALTimerTaskAddDialog(self, timer_task)
if Dialog.exec() == QDialog.DialogCode.Accepted:
updated = Dialog.getTimerTask()
for i, task in enumerate(self.__timer_tasks):
if task["uuid"] == updated["uuid"]:
self.__timer_tasks[i] = updated
@@ -449,19 +449,19 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
):
if timer_task["repeat"]: # when delete a repeat task
msgbox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons(
MsgBox = QMessageBox(self)
MsgBox.setIcon(QMessageBox.Icon.Question)
MsgBox.setWindowTitle("警告 - AutoLibrary")
MsgBox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?")
msgbox.setDetailedText(
MsgBox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?")
MsgBox.setDetailedText(
"以下可重复性任务将被删除:\n"\
"\n"
f"{self.getTimerTaskDetailMessage(timer_task)}"
)
result = msgbox.exec()
result = MsgBox.exec()
if result != QMessageBox.StandardButton.Yes:
return
task_uuid = timer_task["uuid"]
@@ -506,13 +506,13 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
]
repeat_tasks_count = len(repeat_tasks)
if repeat_tasks_count > 0:
msgbox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons(
MsgBox = QMessageBox(self)
MsgBox.setIcon(QMessageBox.Icon.Question)
MsgBox.setWindowTitle("警告 - AutoLibrary")
MsgBox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setText(
MsgBox.setText(
f"存在 {repeat_tasks_count} 个可重复性任务,\n"
"删除可重复性任务将同时删除所有已执行的记录 !\n"
"是否继续 ?"
@@ -520,12 +520,12 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
delete_msgs = [
self.getTimerTaskDetailMessage(x) for x in repeat_tasks
]
msgbox.setDetailedText(
MsgBox.setDetailedText(
"以下可重复性任务将被删除:\n"\
"\n"
f"{"\n\n".join(delete_msgs)}"
)
result = msgbox.exec()
result = MsgBox.exec()
if result != QMessageBox.StandardButton.Yes:
return
# clear all tasks
@@ -537,8 +537,8 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
task: dict
):
dialog = ALTimerTaskHistoryDialog(self, task)
if dialog.exec() == QDialog.DialogCode.Accepted:
Dialog = ALTimerTaskHistoryDialog(self, task)
if Dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit()
def checkTasks(
+15 -15
View File
@@ -51,9 +51,9 @@ class ALUserTreeWidget(QTreeWidget):
self
):
__qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(0, u"\u5206\u7ec4/\u7528\u6237");
self.setHeaderItem(__qtreewidgetitem)
__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))
@@ -81,8 +81,8 @@ class ALUserTreeWidget(QTreeWidget):
self
):
___qtreewidgetitem = self.headerItem()
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
___QTreeWidgetItem = self.headerItem()
___QTreeWidgetItem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
@staticmethod
def isDragPositionValid(
@@ -109,27 +109,27 @@ class ALUserTreeWidget(QTreeWidget):
super().dragMoveEvent(event)
source_item = self.currentItem()
target_item = self.itemAt(event.position().toPoint())
if source_item is None:
SourceItem = self.currentItem()
TargetItem = self.itemAt(event.position().toPoint())
if SourceItem is None:
event.ignore()
return
if source_item.type() == ALUserTreeItemType.GROUP.value:
if target_item is not None:
if SourceItem.type() == ALUserTreeItemType.GROUP.value:
if TargetItem is not None:
event.ignore()
return
elif source_item.type() == ALUserTreeItemType.USER.value:
if target_item is None:
elif SourceItem.type() == ALUserTreeItemType.USER.value:
if TargetItem is None:
event.ignore()
return
if target_item.type() != ALUserTreeItemType.GROUP.value:
if TargetItem.type() != ALUserTreeItemType.GROUP.value:
event.ignore()
return
if target_item.checkState(1) == Qt.CheckState.Unchecked:
if TargetItem.checkState(1) == Qt.CheckState.Unchecked:
event.ignore()
return
if not self.isDragPositionValid(
self.visualItemRect(target_item),
self.visualItemRect(TargetItem),
event.position().toPoint()
):
event.ignore()
-6
View File
@@ -182,14 +182,11 @@ class ALWebDriverDownloadDialog(QDialog):
self.setMaximumHeight(240)
self.setMinimumHeight(240)
self.setWindowTitle("浏览器驱动下载 - AutoLibrary")
self.MainLayout = QVBoxLayout(self)
self.MainLayout.setContentsMargins(5, 5, 5, 5)
self.MainLayout.setSpacing(5)
self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:")
self.MainLayout.addWidget(self.BrowserCountLabel)
self.DriverInfoLayout = QHBoxLayout()
self.DriverInfoLayout.setSpacing(5)
self.DriverComboBox = QComboBox()
@@ -198,7 +195,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.StatusLabel.setFixedSize(32, 32)
self.DriverInfoLayout.addWidget(self.StatusLabel)
self.MainLayout.addLayout(self.DriverInfoLayout)
self.DetailLayout = QVBoxLayout()
self.DetailLayout.setSpacing(5)
self.DetailLayout.setContentsMargins(5, 5, 5, 5)
@@ -211,7 +207,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.PathLabel.setText("路径:未安装")
self.DetailLayout.addWidget(self.PathLabel)
self.MainLayout.addLayout(self.DetailLayout)
self.Line = QFrame()
self.Line.setFrameShape(QFrame.Shape.HLine)
self.Line.setFrameShadow(QFrame.Shadow.Sunken)
@@ -237,7 +232,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setEnabled(False)
self.ControlLayout.addWidget(self.RefreshButton)
self.ControlLayout.addWidget(self.DownloadButton)
self.ControlLayout.addWidget(self.DeleteButton)
+7 -17
View File
@@ -1,19 +1,9 @@
# -*- coding: utf-8 -*-
"""
GUI module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- ALMainWindow: Main window class.
- ALAboutDialog: About dialog class.
- ALConfigWidget: Configuration widget class.
- ALSeatFrame: Seat frame class.
- ALSeatMapView: Seat map view class.
- ALSeatMapTable: Seat map table class.
- ALSeatMapSelectDialog: Seat map select dialog class.
- ALTimerTaskAddDialog: Timer task add dialog class.
- ALAutoScriptOrchDialog: AutoScript orchestration dialog class.
- ALTimerTaskHistoryDialog: Timer task history dialog class.
- ALTimerTaskManageWidget: Timer task manage widget class.
- ALUserTreeWidget: User tree widget class.
- ALMainWorkers: Main workers class.
- ALVersionInfo: Version info class.
"""
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.
"""
+8 -2
View File
@@ -1,3 +1,9 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 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.
"""
GUI resources module for the AutoLibrary project.
"""
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

+1 -48
View File
@@ -19,7 +19,7 @@
<property name="maximumSize">
<size>
<width>800</width>
<height>400</height>
<height>600</height>
</size>
</property>
<property name="windowTitle">
@@ -103,53 +103,6 @@
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="AboutInfoSpaceFrame">
<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="QTextBrowser" name="AboutInfoBrowser">
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="lineWrapMode">
<enum>QTextEdit::LineWrapMode::NoWrap</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
+6 -8
View File
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""
Interfaces module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Defines abstract interfaces (Protocols) and shared type definitions
used across layers to decouple consumers from concrete implementations.
Key components:
- ConfigProvider: Abstract interface for configuration access.
- ConfigType: Enumeration of configuration file types.
- ConfigKey: Type-safe hierarchical key constants for config lookups.
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.
"""
+7 -6
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""
Managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- ConfigManager: Config manager for managing configuration files.
- LogManager: Log manager for logging.
- WebDriverManager: Web driver manager for managing web drivers.
"""
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.
"""
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Config managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- ConfigManager: Config manager for managing configuration files.
"""
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.
"""
@@ -360,6 +360,10 @@ class WebDriverDownloader:
break
if not driver_file:
raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}")
# Ensure executable permissions on Unix systems (zipfile
# extraction does not preserve the execute bit).
if os.name != 'nt':
os.chmod(driver_file, 0o755)
progress_callback(100, 100, 0.0, "解压完成")
self.download_path.unlink()
self._cleanup(driver_file)
+4
View File
@@ -111,6 +111,10 @@ class WebDriverManager:
for driver_info in self.__driver_infos:
driver_path = self._getDriverPath(driver_info)
if driver_path and driver_path.exists() and driver_path.is_file():
# Repair missing execute permission on Unix
# (zip-extracted drivers from older versions).
if os.name != 'nt' and not os.access(str(driver_path), os.X_OK):
os.chmod(str(driver_path), 0o755)
driver_info.driver_path = driver_path
driver_info.driver_status = WebDriverStatus.INSTALLED
+7 -6
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""
Driver managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- WebBrowserDetector: Web browser detector class.
- WebDriverDownloader: Web driver downloader class.
- WebDriverManager: Web driver manager class.
"""
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.
"""
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Log managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- LogManager: Log manager for logging.
"""
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.
"""
+25 -21
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
import os
import queue
from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.common.exceptions import (
TimeoutException,
WebDriverException,
@@ -37,11 +38,11 @@ class AutoLib(MsgBase):
output_queue: queue.Queue,
run_config: dict,
) -> None:
super().__init__(input_queue, output_queue)
super().__init__(input_queue, output_queue)
self.__run_config: dict = run_config
self.__user_config: dict | None = None
self.__driver = None
self.__driver: WebDriver | None = None
self.__driver_type: str = ""
self.__driver_path: str = ""
self.__login_page: LoginPage = None
@@ -58,7 +59,7 @@ class AutoLib(MsgBase):
else:
if not self.__initDriverUrl():
self.close()
raise Exception("浏览器驱动URL初始化失败 !")
raise Exception("浏览器驱动 URL 初始化失败 !")
self.__initPagesServices()
self.__initPagesFlows()
@@ -67,9 +68,10 @@ class AutoLib(MsgBase):
) -> bool:
self._showTrace("正在初始化浏览器驱动......", no_log=True)
web_driver_config: dict = self.__run_config.get("web_driver", None)
self.__driver_type = web_driver_config.get("driver_type", "none")
match self.__driver_type.lower():
driver_config: dict = self.__run_config.get("web_driver", None)
self.__driver_type = driver_config.get("driver_type", "none")
self.__driver_type = self.__driver_type.lower()
match self.__driver_type:
case "edge":
driver_options = webdriver.EdgeOptions()
case "chrome":
@@ -82,10 +84,10 @@ class AutoLib(MsgBase):
self.TraceLevel.WARNING,
)
return False
if not web_driver_config:
if not driver_config:
self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
return False
if web_driver_config.get("headless", False):
if driver_config.get("headless", False):
driver_options.add_argument("--headless")
driver_options.add_argument("--disable-gpu")
driver_options.add_argument("--no-sandbox")
@@ -110,11 +112,11 @@ class AutoLib(MsgBase):
"AppleWebKit/537.36 (KHTML, like Gecko) "\
"Chrome/120.0.0.0 "\
"Safari/537.36"
if self.__driver_type.lower() == "edge":
if self.__driver_type == "edge":
user_agent += " Edg/120.0.0.0"
# set options for firefox
elif self.__driver_type.lower() == "firefox":
elif self.__driver_type == "firefox":
driver_options.set_preference("dom.webdriver.enabled", False)
driver_options.set_preference("useAutomationExtension", False)
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\
@@ -122,14 +124,14 @@ class AutoLib(MsgBase):
driver_options.add_argument(f"user-agent={user_agent}")
# init browser driver
self.__driver_path = web_driver_config.get("driver_path", "")
self.__driver_path = driver_config.get("driver_path", "")
if not self.__driver_path:
self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING)
return False
try:
self.__driver_path = os.path.abspath(self.__driver_path)
service = None
match self.__driver_type.lower():
match self.__driver_type:
case "edge":
service = EdgeService(executable_path=self.__driver_path)
self.__driver = webdriver.Edge(service=service, options=driver_options)
@@ -149,9 +151,6 @@ class AutoLib(MsgBase):
except WebDriverException as e:
self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
return False
except Exception as e:
self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
return False
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
return True
@@ -164,7 +163,7 @@ class AutoLib(MsgBase):
self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR)
return False
url: str = lib_config.get("host_url") + lib_config.get("login_url")
self.__login_page = LoginPage(self.__driver, tracer=self._showTrace)
self.__login_page = LoginPage(self._input_queue, self._output_queue, self.__driver)
self.__driver.set_page_load_timeout(5)
try:
self.__driver.get(url)
@@ -236,6 +235,7 @@ class AutoLib(MsgBase):
# result : -1 - terminate, 0 - success, 1 - failed, 2 - passed
result: int = 2
# login
auto_captcha: bool = login_config.get("auto_captcha", True)
if not self.__login_page.login(
@@ -252,10 +252,11 @@ class AutoLib(MsgBase):
"auto_checkin": run_mode_raw & 0x2,
"auto_renewal": run_mode_raw & 0x4,
}
# reserve
if run_mode["auto_reserve"]:
if self.__record_checker.canReserve(self.__shell, reserve_info["date"]):
if self.__reserve_checker.check(reserve_info):
if self.__reserve_checker.check(reserve_info):
if self.__record_checker.canReserve(self.__shell, reserve_info["date"]):
ctx = ReserveContext(
username=username,
date=reserve_info["date"],
@@ -276,10 +277,10 @@ class AutoLib(MsgBase):
else:
result = 1
else:
result = 1
self._showTrace(f"用户 {username} 无法预约, 已跳过")
result = 2
else:
self._showTrace(f"用户 {username} 无法预约, 已跳过")
result = 2
result = 1
# checkin
last_result: int = result
@@ -320,8 +321,11 @@ class AutoLib(MsgBase):
# logout
if not self.__shell.logout():
self._showTrace(f"用户 {username} 退出登录失败, 尝试直接重载页面")
if not self.__initDriverUrl():
self._showTrace(f"用户 {username} 重载页面失败, 无法继续操作, 该任务已终止 !")
return -1
self._showTrace(f"用户 {username} 已退出登录")
return result
def run(
+28 -42
View File
@@ -7,7 +7,8 @@ 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 typing import Callable, Optional
import queue
from typing import Callable
from selenium.common.exceptions import (
ElementNotInteractableException,
@@ -19,8 +20,10 @@ from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from base.MsgBase import MsgBase
class LoginPage:
class LoginPage(MsgBase):
USERNAME_INPUT = (By.NAME, "username")
PASSWORD_INPUT = (By.NAME, "password")
@@ -36,22 +39,13 @@ class LoginPage:
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver: WebDriver,
tracer: Optional[Callable[..., None]] = None,
) -> None:
super().__init__(input_queue, output_queue)
self._driver: WebDriver = driver
self._tracer: Optional[Callable[..., None]] = tracer
def _trace(
self,
msg: str,
level: int = 20,
no_log: bool = False,
) -> None:
if self._tracer:
self._tracer(msg, level, no_log)
def navigate(
self,
@@ -85,9 +79,7 @@ class LoginPage:
EC.presence_of_element_located(self.CAPTCHA_IMG)
)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
def fillCredentials(
@@ -104,17 +96,21 @@ class LoginPage:
el.clear()
el.send_keys(password)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def getCaptchaImageSrc(
self,
) -> str:
) -> str | None:
captcha_el = self._driver.find_element(*self.CAPTCHA_IMG)
return captcha_el.get_attribute("src")
# return 'None' if captcha image element is not found.
# But the 'get_attribute("src")' also return 'None' if there's no attribute with
# that name, which is not what we want.
try:
captcha_el = self._driver.find_element(*self.CAPTCHA_IMG)
return captcha_el.get_attribute("src")
except NoSuchElementException:
return None
def refreshCaptcha(
self,
@@ -123,10 +119,7 @@ class LoginPage:
try:
self._driver.find_element(*self.CAPTCHA_IMG).click()
return True
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def fillCaptcha(
@@ -139,9 +132,7 @@ class LoginPage:
el.clear()
el.send_keys(captcha_text)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def clickLogin(
@@ -151,10 +142,7 @@ class LoginPage:
try:
self._driver.find_element(*self.LOGIN_BUTTON).click()
return True
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def waitLoginSuccess(
@@ -172,9 +160,7 @@ class LoginPage:
EC.presence_of_element_located(self.SUCCESS_INDICATOR_CONTENT)
)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
def stopPageLoad(
@@ -193,7 +179,7 @@ class LoginPage:
) -> bool:
for attempt in range(max_attempts):
self._trace(
self._showTrace(
f"用户 {username}{attempt + 1} 次尝试登录......",
no_log=True,
)
@@ -204,16 +190,16 @@ class LoginPage:
continue
if not self.fillCaptcha(captcha_text):
continue
self._trace("尝试登录...", no_log=True)
self._showTrace("尝试登录...", no_log=True)
if not self.clickLogin():
continue
if self.waitLoginSuccess():
self._trace(f"用户 {username}{attempt + 1} 次登录成功 !")
self._showTrace(f"用户 {username}{attempt + 1} 次登录成功 !")
return True
else:
self._trace(
self._showTrace(
"登录页面加载失败 ! : "
"用户账号或者密码错误/验证码错误, 具体以页面提示为准",
level=40,
level=self.TraceLevel.ERROR,
)
return False
+40 -29
View File
@@ -14,8 +14,10 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
TimeoutException,
WebDriverException,
)
from pages.ReserveView import ReserveView
@@ -38,6 +40,15 @@ class MainShell:
self._driver = driver
def _clickTab(
self,
locator: tuple,
) -> None:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(locator)
).click()
def gotoReserveView(
self,
) -> ReserveView:
@@ -65,9 +76,7 @@ class MainShell:
try:
self._driver.find_element(*self.TAB_LOGOUT).click()
return True
except NoSuchElementException:
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def waitCheckinButton(
@@ -81,8 +90,6 @@ class MainShell:
return True
except TimeoutException:
return False
except Exception:
return False
def waitExtendButton(
self,
@@ -95,40 +102,50 @@ class MainShell:
return True
except TimeoutException:
return False
except Exception:
return False
def isCheckinButtonDisabled(
self,
) -> bool:
btn = self._driver.find_element(*self.BTN_CHECKIN)
return "disabled" in btn.get_attribute("class")
try:
btn = self._driver.find_element(*self.BTN_CHECKIN)
return "disabled" in btn.get_attribute("class")
except NoSuchElementException:
return True
def isExtendButtonDisabled(
self,
) -> bool:
btn = self._driver.find_element(*self.BTN_EXTEND)
return "disabled" in btn.get_attribute("class")
try:
btn = self._driver.find_element(*self.BTN_EXTEND)
return "disabled" in btn.get_attribute("class")
except NoSuchElementException:
return True
def clickCheckinButton(
self,
) -> None:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_CHECKIN)
)
btn.click()
try:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_CHECKIN)
)
btn.click()
except (TimeoutException, ElementNotInteractableException):
return
def clickExtendButton(
self,
) -> None:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_EXTEND)
)
btn.click()
try:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_EXTEND)
)
btn.click()
except (TimeoutException, ElementNotInteractableException):
return
def enableCheckinButtonByJS(
self,
@@ -154,13 +171,7 @@ class MainShell:
self,
) -> None:
self._driver.refresh()
def _clickTab(
self,
locator: tuple,
) -> None:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(locator)
).click()
try:
self._driver.refresh()
except (TimeoutException, WebDriverException):
return
-6
View File
@@ -44,8 +44,6 @@ class RecordsView:
return self._driver.find_elements(*self.RECORDS_LIST)
except TimeoutException:
return None
except Exception:
return None
def getRecordTimeElement(
self,
@@ -71,8 +69,6 @@ class RecordsView:
)
except TimeoutException:
return False
except Exception:
return False
try:
more_btn = self._driver.find_element(*self.MORE_BTN)
if more_btn.is_displayed() and more_btn.is_enabled():
@@ -82,8 +78,6 @@ class RecordsView:
return False
except (NoSuchElementException, StaleElementReferenceException):
return False
except Exception:
return False
def getRecordText(
self,
+48 -55
View File
@@ -53,6 +53,50 @@ class ReserveView:
self._driver = driver
def _clickOptionByJS(
self,
trigger_id: str,
option_css: str,
) -> bool:
script = f"""
try {{
var trigger = document.getElementById('{trigger_id}');
if (trigger) {{
trigger.click();
var option = document.querySelector("{option_css}");
if (option) {{
option.click();
return true;
}}
return false;
}}
return false;
}} catch (e) {{
return false;
}}
"""
result = self._driver.execute_script(script)
time.sleep(0.1)
return result
def _clickOption(
self,
trigger: tuple,
option: tuple,
) -> bool:
try:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(trigger)
).click()
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(option)
).click()
return True
except (TimeoutException, ElementNotInteractableException):
return False
def selectDate(
self,
date_str: str,
@@ -109,21 +153,15 @@ class ReserveView:
).click()
except (TimeoutException, ElementNotInteractableException):
return None
except Exception:
return None
try:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable((By.ID, self.ROOM_BTN_FMT.format(room=room)))
).click()
except (TimeoutException, ElementNotInteractableException):
return None
except Exception:
return None
try:
return SeatMapDialog(self._driver)
except (TimeoutException):
return None
except Exception:
except TimeoutException:
return None
def submitReserve(
@@ -137,57 +175,12 @@ class ReserveView:
return True
except (TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False
def refresh(
self,
) -> None:
self._driver.refresh()
def _clickOptionByJS(
self,
trigger_id: str,
option_css: str,
) -> bool:
script = f"""
try {{
var trigger = document.getElementById('{trigger_id}');
if (trigger) {{
trigger.click();
var option = document.querySelector("{option_css}");
if (option) {{
option.click();
return true;
}}
return false;
}}
return false;
}} catch (e) {{
return false;
}}
"""
result = self._driver.execute_script(script)
time.sleep(0.1)
return result
def _clickOption(
self,
trigger: tuple,
option: tuple,
) -> bool:
try:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(trigger)
).click()
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(option)
).click()
return True
except (TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False
self._driver.refresh()
except TimeoutException:
return
+10 -23
View File
@@ -7,26 +7,13 @@ 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 pages.AutoLib import AutoLib
from pages.LoginPage import LoginPage
from pages.MainShell import MainShell
from pages.ReserveView import ReserveView
from pages.RecordsView import RecordsView
from pages.components.SeatMapDialog import SeatMapDialog
from pages.components.TimeSelectDialog import TimeSelectDialog
from pages.components.ReserveResultDialog import ReserveResultDialog
from pages.components.CheckinResultDialog import CheckinResultDialog
from pages.components.RenewDialog import RenewDialog
__all__ = [
"AutoLib",
"LoginPage",
"MainShell",
"ReserveView",
"RecordsView",
"SeatMapDialog",
"TimeSelectDialog",
"ReserveResultDialog",
"CheckinResultDialog",
"RenewDialog",
]
from .AutoLib import AutoLib
from .LoginPage import LoginPage
from .MainShell import MainShell
from .ReserveView import ReserveView
from .RecordsView import RecordsView
from .components.SeatMapDialog import SeatMapDialog
from .components.TimeSelectDialog import TimeSelectDialog
from .components.ReserveResultDialog import ReserveResultDialog
from .components.CheckinResultDialog import CheckinResultDialog
from .components.RenewDialog import RenewDialog
+3 -8
View File
@@ -45,9 +45,8 @@ class CheckinResultDialog(Dialog):
self._waitPresence(self.RESULT_MSG)
el = self._find(*self.RESULT_MSG)
return el.text
except (TimeoutException, NoSuchElementException, StaleElementReferenceException):
return ""
except Exception:
except (TimeoutException, NoSuchElementException,
StaleElementReferenceException):
return ""
def getDetails(
@@ -59,8 +58,6 @@ class CheckinResultDialog(Dialog):
return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException):
return []
except Exception:
return []
def clickOk(
self,
@@ -69,7 +66,5 @@ class CheckinResultDialog(Dialog):
try:
self._waitClickable(self.OK_BTN).click()
return True
except (NoSuchElementException, TimeoutException, ElementNotInteractableException):
return False
except Exception:
except (TimeoutException, ElementNotInteractableException):
return False
+16 -13
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
StaleElementReferenceException,
TimeoutException,
)
from selenium.webdriver.common.by import By
@@ -50,9 +51,7 @@ class RenewDialog(Dialog):
self._waitVisible(self.ROOT)
self._waitPresence(self.MESSAGE_HEAD)
self._waitPresence(self.RESULT_MSG)
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
head_msg = self._find(*self.MESSAGE_HEAD).text.strip()
if "警告" in head_msg:
@@ -60,9 +59,7 @@ class RenewDialog(Dialog):
try:
self._waitAllPresence(self.TIME_OPTS)
self._waitPresence(self.OK_BTN)
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
return True
@@ -70,13 +67,19 @@ class RenewDialog(Dialog):
self,
) -> str:
return self._find(*self.MESSAGE_HEAD).text.strip()
try:
return self._find(*self.MESSAGE_HEAD).text.strip()
except (NoSuchElementException, StaleElementReferenceException):
return ""
def getResultMessage(
self,
) -> str:
return self._find(*self.RESULT_MSG).text.strip()
try:
return self._find(*self.RESULT_MSG).text.strip()
except (NoSuchElementException, StaleElementReferenceException):
return ""
def getTimeOptions(
self,
@@ -101,7 +104,10 @@ class RenewDialog(Dialog):
prefer_earlier,
)
if result.selected_index >= 0:
all_time_opts[result.selected_index].click()
try:
all_time_opts[result.selected_index].click()
except (ElementNotInteractableException, StaleElementReferenceException):
return TimeSelectionResult(free_times=result.free_times)
return result
def getOkButton(
@@ -117,8 +123,5 @@ class RenewDialog(Dialog):
try:
self._find(*self.OK_BTN).click()
return True
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
@@ -45,8 +45,6 @@ class ReserveResultDialog(Dialog):
return self._find(*self._titleLocator()).text
except (NoSuchElementException, StaleElementReferenceException):
return ""
except Exception:
return ""
def isSuccess(
self,
@@ -77,5 +75,3 @@ class ReserveResultDialog(Dialog):
return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException):
return []
except Exception:
return []
+1 -7
View File
@@ -43,9 +43,7 @@ class SeatMapDialog(Dialog):
try:
self._waitAllPresence(self.SEAT_ITEMS)
except (NoSuchElementException, TimeoutException):
return None
except Exception:
except TimeoutException:
return None
try:
seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}")
@@ -58,8 +56,6 @@ class SeatMapDialog(Dialog):
except (NoSuchElementException, ValueError, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException):
pass
except Exception:
pass
try:
all_seats = self._findAll(*self.SEAT_ITEMS)
seat_id_upper = seat_id.lstrip('0').upper()
@@ -76,5 +72,3 @@ class SeatMapDialog(Dialog):
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException):
return None
except Exception:
return None
+7 -5
View File
@@ -13,7 +13,8 @@ import logging
from typing import TYPE_CHECKING, Callable, Optional
from selenium.common.exceptions import (
NoSuchElementException,
ElementNotInteractableException,
StaleElementReferenceException,
TimeoutException,
)
from selenium.webdriver.common.by import By
@@ -106,9 +107,7 @@ class TimeSelectDialog(Dialog):
self._waitAllPresence(
(By.CSS_SELECTOR, f"#{time_id} ul li a")
)
except (NoSuchElementException, TimeoutException):
return []
except Exception:
except TimeoutException:
return []
return self._findAll(
By.CSS_SELECTOR,
@@ -133,7 +132,10 @@ class TimeSelectDialog(Dialog):
prefer_earlier,
)
if result.selected_index >= 0:
all_time_opts[result.selected_index].click()
try:
all_time_opts[result.selected_index].click()
except (ElementNotInteractableException, StaleElementReferenceException):
return TimeSelectionResult(free_times=result.free_times)
return result
def selectTimeRange(
+5 -13
View File
@@ -7,16 +7,8 @@ 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 pages.components.SeatMapDialog import SeatMapDialog
from pages.components.TimeSelectDialog import TimeSelectDialog
from pages.components.ReserveResultDialog import ReserveResultDialog
from pages.components.CheckinResultDialog import CheckinResultDialog
from pages.components.RenewDialog import RenewDialog
__all__ = [
"SeatMapDialog",
"TimeSelectDialog",
"ReserveResultDialog",
"CheckinResultDialog",
"RenewDialog",
]
from .SeatMapDialog import SeatMapDialog
from .TimeSelectDialog import TimeSelectDialog
from .ReserveResultDialog import ReserveResultDialog
from .CheckinResultDialog import CheckinResultDialog
from .RenewDialog import RenewDialog
+19 -5
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
import queue
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
TimeoutException,
)
@@ -34,7 +35,7 @@ class CheckinFlow(MsgBase):
self._driver: WebDriver = driver
self._shell: MainShell = shell
def execute(
def _ensureCheckinButton(
self,
username: str,
) -> bool:
@@ -48,7 +49,13 @@ class CheckinFlow(MsgBase):
self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
return False
self._showTrace("签到按钮已启用")
self._shell.clickCheckinButton()
return True
def _processCheckinDialog(
self,
username: str,
) -> bool:
try:
with CheckinResultDialog(self._driver) as dialog:
result_msg = dialog.getResultMessage()
@@ -83,9 +90,16 @@ class CheckinFlow(MsgBase):
dialog.clickOk()
self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR)
return False
except (NoSuchElementException, TimeoutException):
except (TimeoutException, NoSuchElementException, ElementNotInteractableException):
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
return False
except Exception:
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
def execute(
self,
username: str,
) -> bool:
if not self._ensureCheckinButton(username):
return False
self._shell.clickCheckinButton()
return self._processCheckinDialog(username)
+60 -35
View File
@@ -39,19 +39,45 @@ class RenewFlow(MsgBase):
self._driver: WebDriver = driver
self._shell: MainShell = shell
def execute(
def _validateRenewTime(
self,
username: str,
record: dict,
renew_info: dict,
end_time: str,
target_renew_mins: int,
) -> bool:
max_diff = renew_info.get("max_diff", 30)
prefer_earlier = renew_info.get("prefer_early", True)
if target_renew_mins > self.LIBRARY_CLOSE_MINS:
actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time)
if actual_renew_duration <= 0:
self._showTrace(
f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR
)
return False
self._showTrace(
f"续约时间已调整至闭馆时间 "
f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)},"
f"实际续约时长为 "
f"{actual_renew_duration // 60} 小时 "
f"{actual_renew_duration % 60} 分钟"
)
return True
def _computeRenewTarget(
self,
record: dict,
renew_info: dict,
):
end_time = record["time"]["end"]
target_renew_mins = timeStrToMins(end_time) + renew_info.get("expect_duration", 2) * 60
if not self._validateRenewTime(end_time, target_renew_mins):
return False
return None
return target_renew_mins
def _ensureExtendButton(
self,
username: str,
) -> bool:
if not self._shell.waitExtendButton():
self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR)
return False
@@ -61,7 +87,17 @@ class RenewFlow(MsgBase):
f"请连接图书馆网络后重试"
)
return False
self._shell.clickExtendButton()
return True
def _processRenewDialog(
self,
username: str,
record: dict,
target_renew_mins: int,
max_diff: int,
prefer_earlier: bool,
) -> bool:
try:
with RenewDialog(self._driver) as dialog:
if not dialog.waitUntilReady():
@@ -106,37 +142,26 @@ class RenewFlow(MsgBase):
self._showTrace(f"当前可供续约的时间有: {result.free_times}")
self._shell.refresh()
return False
except (NoSuchElementException, TimeoutException) as e:
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
self._shell.refresh()
return False
except (ElementNotInteractableException) as e:
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
self._shell.refresh()
return False
except Exception as e:
except (NoSuchElementException, TimeoutException, ElementNotInteractableException) as e:
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
self._shell.refresh()
return False
def _validateRenewTime(
def execute(
self,
end_time: str,
target_renew_mins: int,
username: str,
record: dict,
renew_info: dict,
) -> bool:
if target_renew_mins > self.LIBRARY_CLOSE_MINS:
actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time)
if actual_renew_duration <= 0:
self._showTrace(
f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR
)
return False
self._showTrace(
f"续约时间已调整至闭馆时间 "
f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)},"
f"实际续约时长为 "
f"{actual_renew_duration // 60} 小时 "
f"{actual_renew_duration % 60} 分钟"
)
return True
max_diff = renew_info.get("max_diff", 30)
prefer_earlier = renew_info.get("prefer_early", True)
target_renew_mins = self._computeRenewTarget(record, renew_info)
if target_renew_mins is None:
return False
if not self._ensureExtendButton(username):
return False
self._shell.clickExtendButton()
return self._processRenewDialog(
username, record, target_renew_mins, max_diff, prefer_earlier,
)
+103 -41
View File
@@ -12,7 +12,6 @@ from dataclasses import dataclass
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
TimeoutException,
)
from selenium.webdriver.remote.webdriver import WebDriver
@@ -59,43 +58,105 @@ class ReserveFlow(MsgBase):
self._driver: WebDriver = driver
self._shell: MainShell = shell
def execute(
def _loadReserveView(
self,
) -> ReserveView | None:
try:
return self._shell.gotoReserveView()
except (TimeoutException, ElementNotInteractableException) as e:
self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR)
return None
def _selectDate(
self,
view: ReserveView,
ctx: ReserveContext,
) -> bool:
submit_reserve = False
reserve_success = False
have_hover_on_page = False
try:
view = self._shell.gotoReserveView()
except (NoSuchElementException, TimeoutException) as e:
self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR)
return False
except Exception as e:
self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR)
return False
if not view.selectDate(ctx.date):
self._showTrace(f"选择日期失败 ! : {ctx.date} 不可用", self.TraceLevel.ERROR)
return False
self._showTrace(f"日期 {ctx.date} 选择成功 !")
return True
def _selectPlace(
self,
view: ReserveView,
) -> bool:
if not view.selectPlace("1"):
self._showTrace("选择预约场所失败 ! : 图书馆 不可用", self.TraceLevel.ERROR)
return False
self._showTrace("预约场所 图书馆 选择成功 !")
return True
def _selectFloor(
self,
view: ReserveView,
ctx: ReserveContext,
) -> bool:
if not view.selectFloor(ctx.floor):
display_floor = ReserveView.FLOOR_MAP.get(ctx.floor, ctx.floor)
self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR)
return False
self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !")
return True
def _selectRoom(
self,
view: ReserveView,
ctx: ReserveContext,
):
seat_map = view.selectRoom(ctx.room)
if seat_map is None:
display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room)
self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
return False
return None
self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !")
have_hover_on_page = True
return seat_map
def _processReserveResult(
self,
) -> bool:
with ReserveResultDialog(self._driver) as result:
if result.isFailure():
self._showTrace("预约失败", self.TraceLevel.ERROR)
elif result.isSuccess():
details = result.getDetailTexts()
if len(details) >= 6:
self._showTrace(
f"\n"
f" 预约成功 !\n"
f" {details[1]}\n"
f" {details[2]}\n"
f" {details[3]}\n"
f" 签到时间 {details[5]}"
)
else:
self._showTrace(
"\n"
" 预约成功 !\n"
" 未找获取到详细信息"
)
return True
else:
self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR)
return False
def _selectSeatAndSubmit(
self,
view: ReserveView,
seat_map,
ctx: ReserveContext,
) -> tuple[bool, bool]:
submit_reserve = False
reserve_success = False
seat_status = seat_map.selectSeat(ctx.seat_id)
if seat_status is None:
self._showTrace(
@@ -115,33 +176,34 @@ class ReserveFlow(MsgBase):
try:
view.submitReserve()
submit_reserve = True
with ReserveResultDialog(self._driver) as result:
if result.isFailure():
self._showTrace("预约失败", self.TraceLevel.ERROR)
elif result.isSuccess():
details = result.getDetailTexts()
if len(details) >= 6:
self._showTrace(
f"\n"
f" 预约成功 !\n"
f" {details[1]}\n"
f" {details[2]}\n"
f" {details[3]}\n"
f" 签到时间 {details[5]}"
)
else:
self._showTrace(
"\n"
" 预约成功 !\n"
" 未找获取到详细信息"
)
reserve_success = True
else:
self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR)
reserve_success = self._processReserveResult()
except (TimeoutException, ElementNotInteractableException):
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
except Exception:
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
return submit_reserve, reserve_success
def execute(
self,
ctx: ReserveContext,
) -> bool:
# reserve flow pipeline:
# date > place > floor > room > seat (begin/end time) > submit > result
view = self._loadReserveView()
if view is None:
return False
if not self._selectDate(view, ctx):
return False
if not self._selectPlace(view):
return False
if not self._selectFloor(view, ctx):
return False
seat_map = self._selectRoom(view, ctx)
if seat_map is None:
return False
have_hover_on_page = True
submit_reserve, reserve_success = self._selectSeatAndSubmit(
view, seat_map, ctx,
)
if not submit_reserve and have_hover_on_page:
view.refresh()
if reserve_success:
+3 -9
View File
@@ -7,12 +7,6 @@ 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 pages.flows.ReserveFlow import ReserveFlow
from pages.flows.CheckinFlow import CheckinFlow
from pages.flows.RenewFlow import RenewFlow
__all__ = [
"ReserveFlow",
"CheckinFlow",
"RenewFlow",
]
from .ReserveFlow import ReserveFlow
from .CheckinFlow import CheckinFlow
from .RenewFlow import RenewFlow
+1 -7
View File
@@ -9,11 +9,5 @@ See the LICENSE file for details.
"""
from pages.strategies.TimeSelectMaker import (
minsToTimeStr,
timeStrToMins
timeStrToMins,
)
__all__ = [
"minsToTimeStr",
"timeStrToMins",
]
+11 -26
View File
@@ -11,11 +11,6 @@ import base64
import queue
import ddddocr
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException,
)
from base.MsgBase import MsgBase
from pages.LoginPage import LoginPage
@@ -38,6 +33,9 @@ class CaptchaSolver(MsgBase):
try:
img_src = login_page.getCaptchaImageSrc()
if img_src is None:
self._showTrace("验证码图片元素定位时发生错误 !", self.TraceLevel.ERROR)
return ""
base64_str = img_src.split(',', 1)[1]
captcha_img = base64.b64decode(base64_str)
captcha_text = self._ocr.classification(captcha_img)
@@ -45,15 +43,9 @@ class CaptchaSolver(MsgBase):
self._showTrace(f"识别到验证码为 : '{captcha_text}'", 20, no_log=True)
if len(captcha_text) != 4:
self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
raise Exception("识别到的验证码长度不等于 4 个字符 !")
return ""
return captcha_text
except (NoSuchElementException, TimeoutException) as e:
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
except (ValueError, OSError) as e:
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
except Exception as e:
except ValueError as e:
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
@@ -61,20 +53,13 @@ class CaptchaSolver(MsgBase):
self,
) -> str:
try:
self._showMsg("请输入验证码:")
captcha_text = self._waitMsg(timeout=15)
self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True)
if len(captcha_text) != 4:
self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
raise Exception("输入的验证码长度不等于 4 个字符 !")
return captcha_text
except ValueError as e:
self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
return ""
except Exception as e:
self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
self._showMsg("请输入验证码:")
captcha_text = self._waitMsg(timeout=15)
self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True)
if len(captcha_text) != 4:
self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
return ""
return captcha_text
def solveCaptcha(
self,
+9 -5
View File
@@ -112,20 +112,21 @@ class RecordChecker(MsgBase):
try:
time_element = records_view.getRecordTimeElement(reservation)
info_elements = records_view.getRecordInfoElements(reservation)
except (NoSuchElementException, TimeoutException, StaleElementReferenceException):
except (NoSuchElementException, StaleElementReferenceException):
return {
"date": "",
"time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""},
}
except Exception:
try:
time_data = self._decodeReserveTime(time_element)
info_data = self._decodeReserveInfo(info_elements)
except StaleElementReferenceException:
return {
"date": "",
"time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""},
}
time_data = self._decodeReserveTime(time_element)
info_data = self._decodeReserveInfo(info_elements)
return {
"date": time_data["date"],
"time": time_data["time"],
@@ -152,7 +153,10 @@ class RecordChecker(MsgBase):
records_view = shell.gotoRecordsView()
for _ in range(max_check_times):
reservations = records_view.loadRecords()
try:
reservations = records_view.loadRecords()
except TimeoutException:
reservations = None
if reservations is None:
return None
for reservation in reservations[checked_count:]:
+11
View File
@@ -88,11 +88,22 @@ class ReserveChecker(MsgBase):
) -> bool:
cur_time = time.strftime("%H:%M", time.localtime())
cur_date = time.strftime("%Y-%m-%d", 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}")
elif reserve_info.get("date") == cur_date:
begin_mins = timeStrToMins(reserve_info["begin_time"]["time"])
cur_mins = timeStrToMins(cur_time)
if begin_mins < cur_mins:
self._showTrace(
f"开始时间 {reserve_info['begin_time']['time']} 已过当前时间 {cur_time}, "
f"自动调整为当前时间",
self.TraceLevel.WARNING,
)
reserve_info["begin_time"]["time"] = cur_time
if "max_diff" not in reserve_info["begin_time"]:
reserve_info["begin_time"]["max_diff"] = 30
self._showTrace("开始时间最大时间差未指定, 自动设置为 30 分钟")
+3 -9
View File
@@ -7,12 +7,6 @@ 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 pages.services.CaptchaSolver import CaptchaSolver
from pages.services.ReserveChecker import ReserveChecker
from pages.services.RecordChecker import RecordChecker
__all__ = [
"CaptchaSolver",
"ReserveChecker",
"RecordChecker",
]
from .CaptchaSolver import CaptchaSolver
from .ReserveChecker import ReserveChecker
from .RecordChecker import RecordChecker
+1 -12
View File
@@ -7,7 +7,7 @@ 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 pages.strategies.TimeSelectMaker import (
from .TimeSelectMaker import (
TimeSelectMaker,
TimeDecisionMaker,
TimeOptionReader,
@@ -17,14 +17,3 @@ from pages.strategies.TimeSelectMaker import (
TimeSelectionResult,
TimeRangeResult,
)
__all__ = [
"TimeSelectMaker",
"TimeDecisionMaker",
"TimeOptionReader",
"ReserveTimeReader",
"RenewTimeReader",
"TimeOption",
"TimeSelectionResult",
"TimeRangeResult",
]
+6 -5
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""
Utils module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
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.
"""