1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 23:43:02 +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: push:
branches: branches:
- main - main
pull_request: pull_request:
branches: branches:
- main - main
@@ -15,7 +14,6 @@ on:
- opened - opened
- synchronize - synchronize
- reopened - reopened
# Allow manual trigger for testing
workflow_dispatch: workflow_dispatch:
# #
@@ -49,11 +47,13 @@ jobs:
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: '3.13' python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirement.txt pip install -r requirements.txt
- name: Solve ddddocr compatibility and copy model files - name: Solve ddddocr compatibility and copy model files
run: | run: |
@@ -125,7 +125,7 @@ jobs:
" binaries=[]," " binaries=[],"
" datas=[" " datas=["
" ('models\\common.onnx', 'ddddocr')," " ('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=[]," " hiddenimports=[],"
" hookspath=[]," " hookspath=[],"
@@ -153,7 +153,7 @@ jobs:
" target_arch=None," " target_arch=None,"
" codesign_identity=None," " codesign_identity=None,"
" entitlements_file=None," " entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico']," " icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico'],"
")" ")"
"" ""
"coll = COLLECT(" "coll = COLLECT("
@@ -169,9 +169,11 @@ jobs:
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8 $specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully" 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 Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n" Write-Host "========================================`n"
shell: pwsh shell: pwsh
- name: Build with PyInstaller - name: Build with PyInstaller
@@ -186,7 +188,7 @@ jobs:
$distDir = "dist/AutoLibrary-$version" $distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip" $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" Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) { if (Test-Path $distDir) {
@@ -210,12 +212,283 @@ jobs:
- name: Upload build summary - name: Upload build summary
run: | run: |
Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append "" | 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 "========================================" | 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 "✓ Pull request build test completed successfully!" | 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 "- Version: ${{ steps.get_version.outputs.VERSION }}" | 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 "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | 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 "- Pull Request #${{ github.event.pull_request.number || 'N/A' }}" | 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 "- 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 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 name: Build
# This workflow compiles the application for Windows platform using PyInstaller, and # This workflow compiles the application for Windows and macOS platforms using
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'. # 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. # It is triggered when called by the release workflow.
@@ -76,20 +79,22 @@ jobs:
run: | run: |
$versionInfoFile = "src/gui/ALVersionInfo.py" $versionInfoFile = "src/gui/ALVersionInfo.py"
Write-Host "Verifying $versionInfoFile content:" Write-Host "Verifying $versionInfoFile content:"
Write-Host "==================================" Write-Host "========================================"
Get-Content $versionInfoFile | Write-Host Get-Content $versionInfoFile | Write-Host
Write-Host "==================================" Write-Host "========================================"
shell: pwsh shell: pwsh
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: '3.13' python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements.txt
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirement.txt pip install -r requirements.txt
- name: Solve ddddocr compatibility and copy model files - name: Solve ddddocr compatibility and copy model files
run: | run: |
@@ -161,7 +166,7 @@ jobs:
" binaries=[]," " binaries=[],"
" datas=[" " datas=["
" ('models\\common.onnx', 'ddddocr')," " ('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=[]," " hiddenimports=[],"
" hookspath=[]," " hookspath=[],"
@@ -189,7 +194,7 @@ jobs:
" target_arch=None," " target_arch=None,"
" codesign_identity=None," " codesign_identity=None,"
" entitlements_file=None," " entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico']," " icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico'],"
")" ")"
"" ""
"coll = COLLECT(" "coll = COLLECT("
@@ -205,9 +210,11 @@ jobs:
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8 $specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully" 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 Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n" Write-Host "========================================`n"
shell: pwsh shell: pwsh
- name: Build with PyInstaller - name: Build with PyInstaller
@@ -222,7 +229,7 @@ jobs:
$distDir = "dist/AutoLibrary-$version" $distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip" $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" Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) { if (Test-Path $distDir) {
@@ -242,16 +249,315 @@ jobs:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64 name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: | path: |
${{ steps.zip_release.outputs.ZIP_NAME }} ${{ 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 - name: Upload build summary
if: ${{ github.event_name != 'workflow_call' }} if: ${{ inputs.is_test == 'true' }}
run: | run: |
Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append "" | 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 "========================================" | 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 "✓ Build test completed successfully!" | 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 "- Version: ${{ steps.get_version.outputs.VERSION }}" | 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 "- Tag: ${{ steps.get_version.outputs.TAG_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 "- 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 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 "✓ File replaced: $FILE_PATH"
echo "" echo ""
echo "Updated file content ===================" echo "========================================"
echo "Updated file content"
echo "========================================"
cat "$FILE_PATH" cat "$FILE_PATH"
echo "========================================" echo "========================================"
@@ -151,3 +153,9 @@ jobs:
COMMIT_SHA=$(git rev-parse --short HEAD) COMMIT_SHA=$(git rev-parse --short HEAD)
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "✓ New commit SHA: $COMMIT_SHA" 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. # Commits version changes to release branch and creates the release tag.
# 4. Build: # 4. Build:
# Compiles the application for Windows platform using PyInstaller, and # Compiles the application for Windows and macOS platforms using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'. # archives the built artifacts.
# - Windows: AutoLibrary.<tag_name>-windows-x86_64.zip
# - macOS: AutoLibrary.<tag_name>-macos-arm64.dmg
# 5. Release: # 5. Release:
# Creates GitHub release with generated artifacts and release notes # Creates GitHub release with generated artifacts and release notes
@@ -47,7 +49,7 @@ on:
jobs: jobs:
# #
# Start : # Start :
# virtual job that indacates the start of the release process # virtual job that indicates the start of the release process
# #
start: start:
@@ -158,7 +160,7 @@ jobs:
needs: needs:
- update-version - update-version
- commit-release - 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 uses: ./.github/workflows/build.yml
permissions: permissions:
contents: write contents: write
@@ -181,12 +183,18 @@ jobs:
contents: write contents: write
steps: steps:
- name: Download artifacts - name: Download Windows artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v6
with: with:
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64 name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64
path: artifacts/ 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 - name: Create release
id: create_release id: create_release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@@ -195,6 +203,7 @@ jobs:
name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }} name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }}
files: | files: |
artifacts/AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64.zip 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 draft: false
prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }} prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }}
generate_release_notes: true generate_release_notes: true
@@ -205,7 +214,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# End : # End :
# virtual job that indacates the end of the release process # virtual job that indicates the end of the release process
# #
end: end:
@@ -227,7 +236,7 @@ jobs:
- release - release
- extract-version - extract-version
- commit-release - 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 runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -267,9 +276,13 @@ jobs:
git checkout ${MAIN_BRANCH} git checkout ${MAIN_BRANCH}
# Show branch status before merge # Show branch status before merge
echo "=== Branch status before merge ===" echo "========================================"
echo "Branch status before merge"
echo "========================================"
git log --oneline --graph --all -5 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" git diff ${MAIN_BRANCH} origin/${BRANCH_NAME} --stat || echo "No differences found"
# Force create a merge commit even if there are no changes # 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]" -m "chore(release): merge ${BRANCH_NAME} to ${MAIN_BRANCH} [auto release commit]"
# Show merge result # Show merge result
echo "=== Merge result ===" echo "========================================"
echo "Merge result"
echo "========================================"
git log --oneline --graph -3 git log --oneline --graph -3
# Push to main # Push to main
@@ -310,6 +325,7 @@ jobs:
echo "## Release Cleanup Summary" >> $GITHUB_STEP_SUMMARY echo "## Release Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "========================================" >> $GITHUB_STEP_SUMMARY
echo "✓ Release completed successfully!" >> $GITHUB_STEP_SUMMARY echo "✓ Release completed successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "### Actions Performed:" >> $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 "Build version file location: $VER_INFO_BUILDFILE"
echo "Commit version file location: $VER_INFO_COMMITFILE" echo "Commit version file location: $VER_INFO_COMMITFILE"
echo "" echo ""
echo "Build version ALVersionInfo.py content =" echo "========================================"
echo "Build version ALVersionInfo.py"
echo "========================================"
cat "$VER_INFO_BUILDFILE" cat "$VER_INFO_BUILDFILE"
echo "========================================"
echo "" echo ""
echo "Commit version ALVersionInfo.py content " echo "========================================"
echo "Commit version ALVersionInfo.py"
echo "========================================"
cat "$VER_INFO_COMMITFILE" cat "$VER_INFO_COMMITFILE"
echo "========================================" echo "========================================"
@@ -140,11 +145,16 @@ jobs:
run: | run: |
if git diff --quiet src/gui/ALVersionInfo.py; then if git diff --quiet src/gui/ALVersionInfo.py; then
echo "has_changes=false" >> $GITHUB_OUTPUT echo "has_changes=false" >> $GITHUB_OUTPUT
echo "! No changes detected in ALVersionInfo.py" echo " No changes detected in ALVersionInfo.py"
else else
echo "has_changes=true" >> $GITHUB_OUTPUT echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✓ ALVersionInfo.py has been modified" echo "✓ ALVersionInfo.py has been modified"
fi 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 - name: Upload modified ALVersionInfo.py ready for build
if: steps.check_changes.outputs.has_changes == 'true' if: steps.check_changes.outputs.has_changes == 'true'
View File
+1 -1
View File
@@ -25,7 +25,7 @@
6. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行,支持设置重复任务 6. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行,支持设置重复任务
7. 驱动管理 - 内置浏览器驱动自动管理,支持自动检测浏览器版本并下载对应驱动,无需手动下载 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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from autoscript.ASEngine import ASEngine from .ASEngine import ASEngine
__all__ = [
"ASEngine",
"createEngine",
"createMockTargetData",
"createAllVariablesTable",
"createTargetVarDefs",
]
__version__ = "1.0.0" # autoscript version
_TARGET_VAR_DEFS = [ _TARGET_VAR_DEFS = [
("USERNAME", "String", ["username"], "用户名"), ("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: This software is provided "as is", without any warranty of any kind.
- MsgBase: Base class for messages. 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: This software is provided "as is", without any warranty of any kind.
- AppInitializer: Application initializer class. 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.QtGui import QIcon
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
QDialog QDialog,
QTabWidget,
QTextBrowser
) )
from gui.ALVersionInfo import ( 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.ui.Ui_ALAboutDialog import Ui_ALAboutDialog
from gui.resources import ALResource 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)) self.LogoIconLabel.setPixmap(QIcon(":/res/icons/AutoLibrary_Logo_64.svg").pixmap(48, 48))
info_text = self.generateAboutText() self.TabWidget = QTabWidget()
self.AboutInfoBrowser.setHtml(info_text) self.TabWidget.setDocumentMode(True)
browser_font = self.AboutInfoBrowser.font() AboutBrowser = QTextBrowser()
browser_font.setFamily("Courier New") AboutBrowser.setHtml(self.generateAboutText())
self.AboutInfoBrowser.setFont(browser_font) AboutBrowser.setOpenExternalLinks(True)
self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) 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( def connectSignals(
self self
@@ -61,33 +77,57 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
) -> str: ) -> str:
os_info = self.getOSInfo() os_info = self.getOSInfo()
run_on = f"{os_info['system']} {os_info['version']} {os_info['architecture']}"
selenium_ver = self.getSeleniumVersion()
about_text = f""" about_text = f"""
<h4>Version Information:</h4> <b style="font-size:14px;">VERSION: {AL_VERSION}</b><br>
Version: {AL_VERSION}<br>
Commit SHA: {AL_COMMIT_SHA}<br> Commit SHA: {AL_COMMIT_SHA}<br>
Commit date: {AL_COMMIT_DATE}<br> Commit date: {AL_COMMIT_DATE}<br>
Build date: {AL_BUILD_DATE}<br> Build date: {AL_BUILD_DATE}<br>
Python version: {platform.python_version()}<br> <br>
Qt version: {self.getQtVersion()}<br> <b style="font-size:14px;">SYSTEM</b><br>
Running on: {run_on}<br>
<h4>System Information:</h4>
Processor: {platform.processor()}<br> Processor: {platform.processor()}<br>
Operating system: {os_info['system']}<br> <br>
System version: {os_info['version']}<br> <b style="font-size:14px;">DEPENDENCIES</b><br>
System architecture: {os_info['architecture']}<br> Python: {platform.python_version()}<br>
Qt(PySide6): {self.getQtVersion()}<br>
<h4>Project Information:</h4> Selenium: {selenium_ver}<br>
License: MIT License<br> <br>
Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br> <b style="font-size:14px;">PROJECT</b><br>
Project website: <a href="https://www.autolibrary.kenanzhu.com" style="text-decoration: none;">https://www.autolibrary.kenanzhu.com</a><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>
<h4>Author Information:</h4> <br>
Developer: KenanZhu<br> <b style="font-size:14px;">AUTHOR</b><br>
Contact: nanoki_zh@163.com<br> Developer/Maintainer: KenanZhu<br>
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><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 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( def getOSInfo(
self self
): ):
@@ -129,13 +169,23 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
except: except:
return "Unknown" return "Unknown"
def getSeleniumVersion(
self
):
try:
import selenium
return selenium.__version__
except Exception:
return "Unknown"
def copyAboutInfo( def copyAboutInfo(
self self
): ):
about_text = self.AboutInfoBrowser.toPlainText() about_text = self.TabWidget.currentWidget().toPlainText()
clipboard = QApplication.clipboard() Clipboard = QApplication.clipboard()
clipboard.setText(about_text) Clipboard.setText(about_text)
original_text = self.CopyButton.text() original_text = self.CopyButton.text()
self.CopyButton.setText("已复制") 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) super().__init__(parent)
self._rules = [] self._rules = []
keywordFmt = QTextCharFormat() KeywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#569CD6")) KeywordFmt.setForeground(QColor("#569CD6"))
keywordFmt.setFontWeight(QFont.Weight.Bold) KeywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in [ for kw in [
"if", "elseif", "else", "end", "then", "if", "elseif", "else", "end", "then",
"and", "or", "not", "and", "or", "not",
"local", "function", "return", "nil", "local", "function", "return", "nil",
]: ]:
self._rules.append((r"\b" + kw + r"\b", keywordFmt)) self._rules.append((r"\b" + kw + r"\b", KeywordFmt))
boolFmt = QTextCharFormat() BoolFmt = QTextCharFormat()
boolFmt.setForeground(QColor("#4FC1FF")) BoolFmt.setForeground(QColor("#4FC1FF"))
boolFmt.setFontWeight(QFont.Weight.Bold) BoolFmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\btrue\b", boolFmt)) self._rules.append((r"\btrue\b", BoolFmt))
self._rules.append((r"\bfalse\b", boolFmt)) self._rules.append((r"\bfalse\b", BoolFmt))
cmpFmt = QTextCharFormat() CmpFmt = QTextCharFormat()
cmpFmt.setForeground(QColor("#C586C0")) CmpFmt.setForeground(QColor("#C586C0"))
cmpFmt.setFontWeight(QFont.Weight.Normal) CmpFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]: for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
self._rules.append((op, cmpFmt)) self._rules.append((op, CmpFmt))
arithFmt = QTextCharFormat() ArithFmt = QTextCharFormat()
arithFmt.setForeground(QColor("#C586C0")) ArithFmt.setForeground(QColor("#C586C0"))
arithFmt.setFontWeight(QFont.Weight.Normal) ArithFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]: for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
self._rules.append((op, arithFmt)) self._rules.append((op, ArithFmt))
funcFmt = QTextCharFormat() FuncFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#DCDCAA")) FuncFmt.setForeground(QColor("#DCDCAA"))
funcFmt.setFontWeight(QFont.Weight.Normal) FuncFmt.setFontWeight(QFont.Weight.Normal)
for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]: for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]:
self._rules.append((r"\b" + fn + r"\b", funcFmt)) self._rules.append((r"\b" + fn + r"\b", FuncFmt))
varFmt = QTextCharFormat() VarFmt = QTextCharFormat()
varFmt.setForeground(QColor("#9CDCFE")) VarFmt.setForeground(QColor("#9CDCFE"))
varFmt.setFontWeight(QFont.Weight.Normal) VarFmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in createAllVariablesTable().items()] var_names = [name for _, (name, _) in createAllVariablesTable().items()]
for var in var_names: for var in var_names:
self._rules.append((r"\b" + var + r"\b", varFmt)) self._rules.append((r"\b" + var + r"\b", VarFmt))
strFmt = QTextCharFormat() StrFmt = QTextCharFormat()
strFmt.setForeground(QColor("#CE9178")) StrFmt.setForeground(QColor("#CE9178"))
strFmt.setFontWeight(QFont.Weight.Normal) StrFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', strFmt)) self._rules.append((r'"[^"]*"', StrFmt))
self._rules.append((r"'[^']*'", strFmt)) self._rules.append((r"'[^']*'", StrFmt))
numFmt = QTextCharFormat() NumFmt = QTextCharFormat()
numFmt.setForeground(QColor("#B5CEA8")) NumFmt.setForeground(QColor("#B5CEA8"))
numFmt.setFontWeight(QFont.Weight.Normal) NumFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+(?:\.\d+)?\b", numFmt)) self._rules.append((r"\b\d+(?:\.\d+)?\b", NumFmt))
commentFmt = QTextCharFormat() CommentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#6A9955")) CommentFmt.setForeground(QColor("#6A9955"))
commentFmt.setFontItalic(True) CommentFmt.setFontItalic(True)
self._rules.append((r"--[^\n]*", commentFmt)) self._rules.append((r"--[^\n]*", CommentFmt))
def highlightBlock( def highlightBlock(
self, self,
@@ -147,22 +147,22 @@ class _DebugResultDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("调试运行结果 - AutoLibrary") self.setWindowTitle("调试运行结果 - AutoLibrary")
self.setMinimumSize(600, 200) self.setMinimumSize(600, 200)
layout = QVBoxLayout(self) DbgLayout = QVBoxLayout(self)
table = QTableWidget(len(changes), 3) DbgTable = QTableWidget(len(changes), 3)
table.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"]) DbgTable.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"])
table.horizontalHeader().setStretchLastSection(True) DbgTable.horizontalHeader().setStretchLastSection(True)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) DbgTable.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) DbgTable.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes): for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes):
label = f"{display_name}: {name}({var_type})" label = f"{display_name}: {name}({var_type})"
table.setItem(row, 0, QTableWidgetItem(label)) DbgTable.setItem(row, 0, QTableWidgetItem(label))
table.setItem(row, 1, QTableWidgetItem(str(before_val))) DbgTable.setItem(row, 1, QTableWidgetItem(str(before_val)))
table.setItem(row, 2, QTableWidgetItem(str(after_val))) DbgTable.setItem(row, 2, QTableWidgetItem(str(after_val)))
layout.addWidget(table) DbgLayout.addWidget(DbgTable)
btnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) DbgBtnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") DbgBtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
btnBox.accepted.connect(self.accept) DbgBtnBox.accepted.connect(self.accept)
layout.addWidget(btnBox) DbgLayout.addWidget(DbgBtnBox)
class _TabToSpacesEditor(QPlainTextEdit): class _TabToSpacesEditor(QPlainTextEdit):
@@ -193,9 +193,9 @@ class ALAutoScriptEditDialog(QDialog):
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
self.textEdit.setPlainText(script) self.TextEdit.setPlainText(script)
self._highlighter = ALScriptHighlighter( self._Highlighter = ALScriptHighlighter(
self.textEdit.document() self.TextEdit.document()
) )
if mockData: if mockData:
self.setMockData(mockData) self.setMockData(mockData)
@@ -206,80 +206,80 @@ class ALAutoScriptEditDialog(QDialog):
self.setWindowTitle("AutoScript 编辑 - AutoLibrary") self.setWindowTitle("AutoScript 编辑 - AutoLibrary")
self.setMinimumSize(660, 600) self.setMinimumSize(660, 600)
layout = QVBoxLayout(self) Layout = QVBoxLayout(self)
layout.setSpacing(3) Layout.setSpacing(3)
layout.setContentsMargins(3, 3, 3, 3) Layout.setContentsMargins(3, 3, 3, 3)
toolbarLayout = QHBoxLayout() ToolbarLayout = QHBoxLayout()
self.zoomInBtn = QPushButton("") self.ZoomInBtn = QPushButton("")
self.zoomInBtn.setFixedSize(25, 25) self.ZoomInBtn.setFixedSize(25, 25)
self.zoomOutBtn = QPushButton("") self.ZoomOutBtn = QPushButton("")
self.zoomOutBtn.setFixedSize(25, 25) self.ZoomOutBtn.setFixedSize(25, 25)
self.zoomResetBtn = QPushButton("") self.ZoomResetBtn = QPushButton("")
self.zoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg")) self.ZoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
self.zoomResetBtn.setIconSize(QSize(20, 20)) self.ZoomResetBtn.setIconSize(QSize(20, 20))
self.zoomResetBtn.setFixedSize(25, 25) self.ZoomResetBtn.setFixedSize(25, 25)
self.zoomResetBtn.setToolTip("重置缩放") self.ZoomResetBtn.setToolTip("重置缩放")
self.zoomLabel = QLabel(f"{self._fontSize}px") self.ZoomLabel = QLabel(f"{self._fontSize}px")
self.zoomLabel.setFixedHeight(25) self.ZoomLabel.setFixedHeight(25)
self.orchBtn = QPushButton("编排") self.OrchBtn = QPushButton("编排")
self.orchBtn.setFixedHeight(25) self.OrchBtn.setFixedHeight(25)
self.orchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置") self.OrchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
toolbarLayout.addWidget(self.orchBtn) ToolbarLayout.addWidget(self.OrchBtn)
self.debugBtn = QPushButton("▶ 调试运行") self.DebugBtn = QPushButton("▶ 调试运行")
self.debugBtn.setFixedHeight(25) self.DebugBtn.setFixedHeight(25)
self.debugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化") self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
toolbarLayout.addWidget(self.debugBtn) ToolbarLayout.addWidget(self.DebugBtn)
sep = QFrame() Sep = QFrame()
sep.setFrameShape(QFrame.Shape.VLine) Sep.setFrameShape(QFrame.Shape.VLine)
sep.setFrameShadow(QFrame.Shadow.Sunken) Sep.setFrameShadow(QFrame.Shadow.Sunken)
sep.setFixedWidth(1) Sep.setFixedWidth(1)
toolbarLayout.addWidget(sep) ToolbarLayout.addWidget(Sep)
toolbarLayout.addWidget(self.zoomInBtn) ToolbarLayout.addWidget(self.ZoomInBtn)
toolbarLayout.addWidget(self.zoomOutBtn) ToolbarLayout.addWidget(self.ZoomOutBtn)
toolbarLayout.addWidget(self.zoomResetBtn) ToolbarLayout.addWidget(self.ZoomResetBtn)
toolbarLayout.addWidget(self.zoomLabel) ToolbarLayout.addWidget(self.ZoomLabel)
toolbarLayout.addStretch() ToolbarLayout.addStretch()
self.copyBtn = QPushButton("") self.CopyBtn = QPushButton("")
self.copyBtn.setIcon(QIcon(":/res/icons/Copy.svg")) self.CopyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
self.copyBtn.setIconSize(QSize(20, 20)) self.CopyBtn.setIconSize(QSize(20, 20))
self.copyBtn.setFixedSize(25, 25) self.CopyBtn.setFixedSize(25, 25)
self.copyBtn.setToolTip("复制脚本") self.CopyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self.copyBtn) ToolbarLayout.addWidget(self.CopyBtn)
layout.addLayout(toolbarLayout) Layout.addLayout(ToolbarLayout)
self.textEdit = _TabToSpacesEditor(self) self.TextEdit = _TabToSpacesEditor(self)
self.textEdit.setTabStopDistance(40) self.TextEdit.setTabStopDistance(40)
self.textEdit.setLineWrapMode( self.TextEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap QPlainTextEdit.LineWrapMode.NoWrap
) )
self.textEdit.setStyleSheet( self.TextEdit.setStyleSheet(
"QPlainTextEdit {" "QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;" " font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;" f" font-size: {self._fontSize}px;"
"}" "}"
) )
layout.addWidget(self.textEdit) Layout.addWidget(self.TextEdit)
self.createButtonPanel(layout) self.createButtonPanel(Layout)
self.btnBox = QDialogButtonBox( self.BtnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Cancel
) )
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
layout.addWidget(self.btnBox) Layout.addWidget(self.BtnBox)
def createButtonPanel( def createButtonPanel(
self, self,
parent_layout ParentLayout
): ):
splitter = QSplitter(Qt.Orientation.Horizontal) Splitter = QSplitter(Qt.Orientation.Horizontal)
tabWidget = QTabWidget() TabWidget = QTabWidget()
tabWidget.setMaximumHeight(150) TabWidget.setMaximumHeight(150)
basicWidget = QWidget() BasicWidget = QWidget()
basicLayout = QGridLayout(basicWidget) BasicLayout = QGridLayout(BasicWidget)
basicLayout.setSpacing(4) BasicLayout.setSpacing(4)
basicLayout.setContentsMargins(4, 4, 4, 4) BasicLayout.setContentsMargins(4, 4, 4, 4)
basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [ controlButtons = [
("如果 (if...)", "if then\n \nend"), ("如果 (if...)", "if then\n \nend"),
("再如果 (elseif...)", "elseif then\n "), ("再如果 (elseif...)", "elseif then\n "),
@@ -287,22 +287,22 @@ class ALAutoScriptEditDialog(QDialog):
("结束 (end)", "end"), ("结束 (end)", "end"),
("跳过 (pass)", "-- pass"), ("跳过 (pass)", "-- pass"),
] ]
self.addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3) self.addButtonsToGrid(BasicLayout, controlButtons, 0, 0, 3)
assignButtons = [ assignButtons = [
("赋值 (=)", " = "), ("赋值 (=)", " = "),
] ]
self.addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3) self.addButtonsToGrid(BasicLayout, assignButtons, 1, 2, 3)
tabWidget.addTab(basicWidget, "基本语法") TabWidget.addTab(BasicWidget, "基本语法")
operatorWidget = QWidget() OperatorWidget = QWidget()
operatorLayout = QGridLayout(operatorWidget) OperatorLayout = QGridLayout(OperatorWidget)
operatorLayout.setSpacing(4) OperatorLayout.setSpacing(4)
operatorLayout.setContentsMargins(4, 4, 4, 4) OperatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [ arithmeticButtons = [
("加 (+)", " + "), ("加 (+)", " + "),
("减 (-)", " - "), ("减 (-)", " - "),
] ]
self.addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3) self.addButtonsToGrid(OperatorLayout, arithmeticButtons, 0, 0, 3)
compareButtons = [ compareButtons = [
("等于 (==)", " == "), ("等于 (==)", " == "),
("不等于 (~=)", " ~= "), ("不等于 (~=)", " ~= "),
@@ -311,50 +311,50 @@ class ALAutoScriptEditDialog(QDialog):
("大于等于 (>=)", " >= "), ("大于等于 (>=)", " >= "),
("小于等于 (<=)", " <= "), ("小于等于 (<=)", " <= "),
] ]
self.addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3) self.addButtonsToGrid(OperatorLayout, compareButtons, 1, 0, 3)
logic_buttons = [ logic_buttons = [
("且 (and)", " and "), ("且 (and)", " and "),
("或 (or)", " or "), ("或 (or)", " or "),
] ]
self.addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3) self.addButtonsToGrid(OperatorLayout, logic_buttons, 2, 0, 3)
tabWidget.addTab(operatorWidget, "运算符") TabWidget.addTab(OperatorWidget, "运算符")
literalWidget = QWidget() LiteralWidget = QWidget()
literalLayout = QGridLayout(literalWidget) LiteralLayout = QGridLayout(LiteralWidget)
literalLayout.setSpacing(4) LiteralLayout.setSpacing(4)
literalLayout.setContentsMargins(4, 4, 4, 4) LiteralLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) LiteralLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [ bool_buttons = [
("真 (true)", "true"), ("真 (true)", "true"),
("假 (false)", "false"), ("假 (false)", "false"),
] ]
self.addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3) self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3)
dateTimeButtons = [ dateTimeButtons = [
("日期", 'date(2026, 1, 1)'), ("日期", 'date(2026, 1, 1)'),
("时间", 'time(0, 0)'), ("时间", 'time(0, 0)'),
] ]
self.addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3) self.addButtonsToGrid(LiteralLayout, dateTimeButtons, 1, 0, 3)
hintButtons = [ hintButtons = [
("字符串", '"请输入文本"'), ("字符串", '"请输入文本"'),
("数字", "123"), ("数字", "123"),
("注释", "-- 请输入注释"), ("注释", "-- 请输入注释"),
] ]
self.addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3) self.addButtonsToGrid(LiteralLayout, hintButtons, 2, 0, 3)
tabWidget.addTab(literalWidget, "字面量") TabWidget.addTab(LiteralWidget, "字面量")
varWidget = QWidget() VarWidget = QWidget()
varLayout = QGridLayout(varWidget) VarLayout = QGridLayout(VarWidget)
varLayout.setSpacing(4) VarLayout.setSpacing(4)
varLayout.setContentsMargins(4, 4, 4, 4) VarLayout.setContentsMargins(4, 4, 4, 4)
varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
varButtons = [ varButtons = [
(display_name, name) for display_name, (name, _) in createAllVariablesTable().items() (display_name, name) for display_name, (name, _) in createAllVariablesTable().items()
] ]
self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3) self.addButtonsToGrid(VarLayout, varButtons, 0, 0, 3)
tabWidget.addTab(varWidget, "变量") TabWidget.addTab(VarWidget, "变量")
funcWidget = QWidget() FuncWidget = QWidget()
funcLayout = QGridLayout(funcWidget) FuncLayout = QGridLayout(FuncWidget)
funcLayout.setSpacing(4) FuncLayout.setSpacing(4)
funcLayout.setContentsMargins(4, 4, 4, 4) FuncLayout.setContentsMargins(4, 4, 4, 4)
funcLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
funcButtons = [ funcButtons = [
("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"), ("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"),
("timenow()", "timenow()", "返回当前时间在一天中的分钟数"), ("timenow()", "timenow()", "返回当前时间在一天中的分钟数"),
@@ -362,22 +362,22 @@ class ALAutoScriptEditDialog(QDialog):
("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"), ("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"),
] ]
for i, (text, template, tooltip) in enumerate(funcButtons): for i, (text, template, tooltip) in enumerate(funcButtons):
btn = QPushButton(text) Btn = QPushButton(text)
btn.setProperty("template", template) Btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate) Btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100) Btn.setFixedWidth(100)
btn.setFixedHeight(25) Btn.setFixedHeight(25)
btn.setToolTip(tooltip) Btn.setToolTip(tooltip)
funcLayout.addWidget(btn, i // 2, i % 2) FuncLayout.addWidget(Btn, i // 2, i % 2)
tabWidget.addTab(funcWidget, "工具函数") TabWidget.addTab(FuncWidget, "工具函数")
mockPanel = self.createMockPanel() MockPanel = self.createMockPanel()
mockPanel.setMinimumWidth(260) MockPanel.setMinimumWidth(260)
splitter.addWidget(tabWidget) Splitter.addWidget(TabWidget)
splitter.addWidget(mockPanel) Splitter.addWidget(MockPanel)
splitter.setStretchFactor(0, 1) Splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 1) Splitter.setStretchFactor(1, 1)
splitter.setSizes([530, 530]) Splitter.setSizes([530, 530])
parent_layout.addWidget(splitter) ParentLayout.addWidget(Splitter)
def addButtonsToGrid( def addButtonsToGrid(
self, self,
@@ -392,13 +392,13 @@ class ALAutoScriptEditDialog(QDialog):
row = start_row row = start_row
for btn_text, template in buttons: for btn_text, template in buttons:
btn = QPushButton(btn_text) Btn = QPushButton(btn_text)
btn.setProperty("template", template) Btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate) Btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100) Btn.setFixedWidth(100)
btn.setFixedHeight(25) Btn.setFixedHeight(25)
btn.setToolTip(f"插入: {template}") Btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(btn, row, col) grid_layout.addWidget(Btn, row, col)
col += 1 col += 1
if col >= start_col + max_columns: if col >= start_col + max_columns:
col = start_col col = start_col
@@ -408,10 +408,10 @@ class ALAutoScriptEditDialog(QDialog):
self self
) -> QGroupBox: ) -> QGroupBox:
group = QGroupBox("模拟目标数据") Group = QGroupBox("模拟目标数据")
form = QFormLayout(group) Form = QFormLayout(Group)
form.setSpacing(4) Form.setSpacing(4)
form.setContentsMargins(5, 10, 5, 5) Form.setContentsMargins(5, 10, 5, 5)
self._mockWidgets = {} self._mockWidgets = {}
mockData = createMockTargetData() mockData = createMockTargetData()
for name, var_type, key_path, display_name in createTargetVarDefs(): for name, var_type, key_path, display_name in createTargetVarDefs():
@@ -419,11 +419,11 @@ class ALAutoScriptEditDialog(QDialog):
for key in key_path: for key in key_path:
d = d[key] d = d[key]
default = d default = d
widget = self.makeMockInput(var_type, default) Widget = self.makeMockInput(var_type, default)
label = QLabel(f"{display_name}: {name}({var_type})") Label = QLabel(f"{display_name}: {name}({var_type})")
form.addRow(label, widget) Form.addRow(Label, Widget)
self._mockWidgets[name] = (widget, var_type, key_path) self._mockWidgets[name] = (Widget, var_type, key_path)
return group return Group
def makeMockInput( def makeMockInput(
self, self,
@@ -432,41 +432,41 @@ class ALAutoScriptEditDialog(QDialog):
) -> QWidget: ) -> QWidget:
if var_type == "String": if var_type == "String":
w = QLineEdit() W = QLineEdit()
w.setText(str(default)) W.setText(str(default))
return w return W
if var_type == "Boolean": if var_type == "Boolean":
w = QComboBox() W = QComboBox()
w.addItems(["", ""]) W.addItems(["", ""])
w.setCurrentIndex(0 if default else 1) W.setCurrentIndex(0 if default else 1)
return w return W
if var_type == "Date": if var_type == "Date":
w = QDateEdit() W = QDateEdit()
w.setCalendarPopup(True) W.setCalendarPopup(True)
w.setDisplayFormat("yyyy-MM-dd") W.setDisplayFormat("yyyy-MM-dd")
w.setDate(QDate.fromString(str(default), "yyyy-MM-dd")) W.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
return w return W
if var_type == "Time": if var_type == "Time":
w = QTimeEdit() W = QTimeEdit()
w.setDisplayFormat("HH:mm") W.setDisplayFormat("HH:mm")
w.setTime(QTime.fromString(str(default), "HH:mm")) W.setTime(QTime.fromString(str(default), "HH:mm"))
return w return W
if var_type == "Int": if var_type == "Int":
w = QSpinBox() W = QSpinBox()
w.setMinimum(-999999) W.setMinimum(-999999)
w.setMaximum(999999) W.setMaximum(999999)
w.setValue(int(default) if default else 0) W.setValue(int(default) if default else 0)
return w return W
if var_type == "Float": if var_type == "Float":
w = QDoubleSpinBox() W = QDoubleSpinBox()
w.setMinimum(-999999.0) W.setMinimum(-999999.0)
w.setMaximum(999999.0) W.setMaximum(999999.0)
w.setDecimals(2) W.setDecimals(2)
w.setValue(float(default) if default else 0.0) W.setValue(float(default) if default else 0.0)
return w return W
w = QLineEdit() W = QLineEdit()
w.setText(str(default)) W.setText(str(default))
return w return W
def getMockData( def getMockData(
self self
@@ -541,45 +541,45 @@ class ALAutoScriptEditDialog(QDialog):
self self
): ):
self.btnBox.accepted.connect(self.accept) self.BtnBox.accepted.connect(self.accept)
self.btnBox.rejected.connect(self.reject) self.BtnBox.rejected.connect(self.reject)
self.orchBtn.clicked.connect(self.onOpenOrchDialog) self.OrchBtn.clicked.connect(self.onOpenOrchDialog)
self.debugBtn.clicked.connect(self.onDebugRun) self.DebugBtn.clicked.connect(self.onDebugRun)
self.zoomInBtn.clicked.connect(self.onZoomIn) self.ZoomInBtn.clicked.connect(self.onZoomIn)
self.zoomOutBtn.clicked.connect(self.onZoomOut) self.ZoomOutBtn.clicked.connect(self.onZoomOut)
self.zoomResetBtn.clicked.connect(self.onZoomReset) self.ZoomResetBtn.clicked.connect(self.onZoomReset)
self.copyBtn.clicked.connect(self.onCopy) self.CopyBtn.clicked.connect(self.onCopy)
def getScript( def getScript(
self self
) -> str: ) -> str:
return self.textEdit.toPlainText() return self.TextEdit.toPlainText()
def updateFontSize( def updateFontSize(
self self
): ):
self.textEdit.setStyleSheet( self.TextEdit.setStyleSheet(
"QPlainTextEdit {" "QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;" " font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;" f" font-size: {self._fontSize}px;"
"}" "}"
) )
self.zoomLabel.setText(f"{self._fontSize}px") self.ZoomLabel.setText(f"{self._fontSize}px")
@Slot() @Slot()
def insertTemplate( def insertTemplate(
self self
): ):
btn = self.sender() Btn = self.sender()
if not isinstance(btn, QPushButton): if not isinstance(Btn, QPushButton):
return return
template = btn.property("template") template = Btn.property("template")
if not template: if not template:
return return
cursor = self.textEdit.textCursor() cursor = self.TextEdit.textCursor()
cursor.insertText(template) cursor.insertText(template)
@Slot() @Slot()
@@ -611,11 +611,11 @@ class ALAutoScriptEditDialog(QDialog):
self self
): ):
clipboard = QApplication.clipboard() Clipboard = QApplication.clipboard()
clipboard.setText(self.textEdit.toPlainText()) Clipboard.setText(self.TextEdit.toPlainText())
self.copyBtn.setEnabled(False) self.CopyBtn.setEnabled(False)
QTimer.singleShot(2000, lambda: ( QTimer.singleShot(2000, lambda: (
self.copyBtn.setEnabled(True) self.CopyBtn.setEnabled(True)
)) ))
@Slot() @Slot()
@@ -624,20 +624,20 @@ class ALAutoScriptEditDialog(QDialog):
): ):
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
dlg = ALAutoScriptOrchDialog(self) Dlg = ALAutoScriptOrchDialog(self)
if dlg.exec() == QDialog.DialogCode.Accepted: if Dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript() script = Dlg.getScript()
if script: if script:
cursor = self.textEdit.textCursor() cursor = self.TextEdit.textCursor()
cursor.insertText(script) cursor.insertText(script)
dlg.deleteLater() Dlg.deleteLater()
@Slot() @Slot()
def onDebugRun( def onDebugRun(
self self
): ):
script = self.textEdit.toPlainText().strip() script = self.TextEdit.toPlainText().strip()
if not script: if not script:
QMessageBox.warning(self, "提示", "脚本内容为空。") QMessageBox.warning(self, "提示", "脚本内容为空。")
return return
@@ -664,6 +664,6 @@ class ALAutoScriptEditDialog(QDialog):
if not changes: if not changes:
QMessageBox.information(self, "调试运行", "目标变量未发生变化。") QMessageBox.information(self, "调试运行", "目标变量未发生变化。")
return return
dlg = _DebugResultDialog(changes, self) Dlg = _DebugResultDialog(changes, self)
dlg.exec() Dlg.exec()
dlg.deleteLater() 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; }" "margin-top: 5px; padding-top: 5px; }"
) )
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mainLayout = QVBoxLayout(self) MainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6) MainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8) MainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout() HeaderLayout = QHBoxLayout()
headerLayout.setSpacing(8) HeaderLayout.setSpacing(8)
self.typeCombo = QComboBox(self) self.TypeCombo = QComboBox(self)
self.typeCombo.addItem("IF", "IF") self.TypeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF") self.TypeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE") self.TypeCombo.addItem("ELSE", "ELSE")
self.typeCombo.setFixedHeight(25) self.TypeCombo.setFixedHeight(25)
if self.blockIndex == 0: if self.blockIndex == 0:
self.typeCombo.setEnabled(False) self.TypeCombo.setEnabled(False)
headerLayout.addWidget(QLabel("类型:", self)) HeaderLayout.addWidget(QLabel("类型:", self))
headerLayout.addWidget(self.typeCombo) HeaderLayout.addWidget(self.TypeCombo)
headerLayout.addStretch() HeaderLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块", self) self.DeleteBlockBtn = QPushButton("删除此块", self)
self.deleteBlockBtn.setStyleSheet("color: red;") self.DeleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25) self.DeleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn) HeaderLayout.addWidget(self.DeleteBlockBtn)
mainLayout.addLayout(headerLayout) MainLayout.addLayout(HeaderLayout)
self.conditionWidget = QWidget(self) self.ConditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy( self.ConditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred QSizePolicy.Preferred, QSizePolicy.Preferred
) )
condLayout = QVBoxLayout(self.conditionWidget) CondLayout = QVBoxLayout(self.ConditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4) CondLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6) CondLayout.setSpacing(6)
self.condRowsLayout = QVBoxLayout() self.CondRowsLayout = QVBoxLayout()
self.condRowsLayout.setSpacing(4) self.CondRowsLayout.setSpacing(4)
condLayout.addLayout(self.condRowsLayout) CondLayout.addLayout(self.CondRowsLayout)
self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget) self.AddCondBtn = QPushButton("+ 添加条件", self.ConditionWidget)
self.addCondBtn.setFixedHeight(25) self.AddCondBtn.setFixedHeight(25)
condLayout.addWidget(self.addCondBtn) CondLayout.addWidget(self.AddCondBtn)
mainLayout.addWidget(self.conditionWidget) MainLayout.addWidget(self.ConditionWidget)
self.actionLabel = QLabel("执行步骤:", self) self.ActionLabel = QLabel("执行步骤:", self)
self.actionLabel.setFixedHeight(25) self.ActionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel) MainLayout.addWidget(self.ActionLabel)
self.actionsLayout = QVBoxLayout() self.ActionsLayout = QVBoxLayout()
self.actionsLayout.setSpacing(4) self.ActionsLayout.setSpacing(4)
mainLayout.addLayout(self.actionsLayout) MainLayout.addLayout(self.ActionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤", self) self.AddActionBtn = QPushButton("+ 添加执行步骤", self)
self.addActionBtn.setFixedHeight(25) self.AddActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn) MainLayout.addWidget(self.AddActionBtn)
self.setUpdatesEnabled(True) self.setUpdatesEnabled(True)
def connectSignals( def connectSignals(
self self
): ):
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged) self.TypeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addCondBtn.clicked.connect(self.addConditionRow) self.AddCondBtn.clicked.connect(self.addConditionRow)
self.addActionBtn.clicked.connect(self.addActionStep) self.AddActionBtn.clicked.connect(self.addActionStep)
def addInitialConditionRow( def addInitialConditionRow(
self self
): ):
row = ConditionRowFrame( Row = ConditionRowFrame(
self._varMgr, self.blockIndex, self._varMgr, self.blockIndex,
isFirst=True, parent=self isFirst=True, parent=self
) )
self._conditionRows.append(row) self._conditionRows.append(Row)
self.condRowsLayout.addWidget(row) self.CondRowsLayout.addWidget(Row)
def addConditionRow( def addConditionRow(
self self
): ):
row = ConditionRowFrame( Row = ConditionRowFrame(
self._varMgr, self.blockIndex, self._varMgr, self.blockIndex,
isFirst=False, parent=self isFirst=False, parent=self
) )
row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row)) Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row))
self._conditionRows.append(row) self._conditionRows.append(Row)
self.condRowsLayout.addWidget(row) self.CondRowsLayout.addWidget(Row)
def removeConditionRow( def removeConditionRow(
self, self,
@@ -140,7 +140,7 @@ class ConditionalBlock(QGroupBox):
if row in self._conditionRows and len(self._conditionRows) > 1: if row in self._conditionRows and len(self._conditionRows) > 1:
self._conditionRows.remove(row) self._conditionRows.remove(row)
self.condRowsLayout.removeWidget(row) self.CondRowsLayout.removeWidget(row)
row.hide() row.hide()
row.deleteLater() row.deleteLater()
@@ -148,10 +148,10 @@ class ConditionalBlock(QGroupBox):
self self
): ):
step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self) Step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step)) Step.DeleteBtn.clicked.connect(lambda: self.removeActionStep(Step))
self._actionWidgets.append(step) self._actionWidgets.append(Step)
self.actionsLayout.addWidget(step) self.ActionsLayout.addWidget(Step)
def removeActionStep( def removeActionStep(
self, self,
@@ -160,7 +160,7 @@ class ConditionalBlock(QGroupBox):
if step in self._actionWidgets: if step in self._actionWidgets:
self._actionWidgets.remove(step) self._actionWidgets.remove(step)
self.actionsLayout.removeWidget(step) self.ActionsLayout.removeWidget(step)
step.hide() step.hide()
step.deleteLater() step.deleteLater()
@@ -168,7 +168,7 @@ class ConditionalBlock(QGroupBox):
self self
) -> str: ) -> str:
return self.typeCombo.currentData() return self.TypeCombo.currentData()
def getConditionRows( def getConditionRows(
self self
@@ -239,18 +239,18 @@ class ConditionalBlock(QGroupBox):
prevType: str | None prevType: str | None
): ):
model = self.typeCombo.model() model = self.TypeCombo.model()
if model is None: if model is None:
return return
for data in ("ELSE IF", "ELSE"): for data in ("ELSE IF", "ELSE"):
idx = self.typeCombo.findData(data) idx = self.TypeCombo.findData(data)
if idx < 0: if idx < 0:
continue continue
item = model.item(idx) item = model.item(idx)
shouldEnable = prevType != "ELSE" shouldEnable = prevType != "ELSE"
item.setEnabled(shouldEnable) item.setEnabled(shouldEnable)
if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"): if prevType == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"):
self.typeCombo.setCurrentIndex(0) self.TypeCombo.setCurrentIndex(0)
@Slot(int) @Slot(int)
def onTypeChanged( def onTypeChanged(
@@ -258,8 +258,8 @@ class ConditionalBlock(QGroupBox):
_idx _idx
): ):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF") isCond = self.TypeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond) self.ConditionWidget.setVisible(isCond)
self.actionLabel.setText( self.ActionLabel.setText(
"执行步骤:" if isCond else "ELSE 执行步骤:" "执行步骤:" if isCond else "ELSE 执行步骤:"
) )
+35 -35
View File
@@ -40,7 +40,7 @@ class ALAutoScriptOrchDialog(QDialog):
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
self.addBlock() self.addBlock()
self.scrollLayout.addStretch() self.ScrollLayout.addStretch()
def setupUi( def setupUi(
self self
@@ -49,33 +49,33 @@ class ALAutoScriptOrchDialog(QDialog):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary") self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(640, 600) self.setMinimumSize(640, 600)
self.setModal(True) self.setModal(True)
mainLayout = QVBoxLayout(self) MainLayout = QVBoxLayout(self)
scroll = QScrollArea() Scroll = QScrollArea()
scroll.setWidgetResizable(True) Scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.NoFrame) Scroll.setFrameShape(QFrame.NoFrame)
scrollContent = QWidget() ScrollContent = QWidget()
self.scrollLayout = QVBoxLayout(scrollContent) self.ScrollLayout = QVBoxLayout(ScrollContent)
self.scrollLayout.setSpacing(5) self.ScrollLayout.setSpacing(5)
scroll.setWidget(scrollContent) Scroll.setWidget(ScrollContent)
mainLayout.addWidget(scroll) MainLayout.addWidget(Scroll)
self.addBlockBtn = QPushButton("+ 添加判断块") self.AddBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25) self.AddBlockBtn.setFixedHeight(25)
mainLayout.addWidget(self.addBlockBtn) MainLayout.addWidget(self.AddBlockBtn)
self.btnBox = QDialogButtonBox( self.BtnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Cancel
) )
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox) MainLayout.addWidget(self.BtnBox)
def connectSignals( def connectSignals(
self self
): ):
self.btnBox.accepted.connect(self.onAccept) self.BtnBox.accepted.connect(self.onAccept)
self.btnBox.rejected.connect(self.reject) self.BtnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock) self.AddBlockBtn.clicked.connect(self.addBlock)
def updateBlockTypeRestrictions( def updateBlockTypeRestrictions(
self self
@@ -90,24 +90,24 @@ class ALAutoScriptOrchDialog(QDialog):
self self
): ):
block = ConditionalBlock( Block = ConditionalBlock(
len(self._blocks), self._varMgr, parent=self len(self._blocks), self._varMgr, parent=self
) )
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block)) Block.DeleteBlockBtn.clicked.connect(lambda: self.removeBlock(Block))
block.typeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions) Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
block.addActionStep() Block.addActionStep()
self._blocks.append(block) self._blocks.append(Block)
self.updateBlockTypeRestrictions() self.updateBlockTypeRestrictions()
if self.scrollLayout.count() > 0: if self.ScrollLayout.count() > 0:
lastItem = self.scrollLayout.itemAt( lastItem = self.ScrollLayout.itemAt(
self.scrollLayout.count() - 1 self.ScrollLayout.count() - 1
) )
if lastItem and lastItem.spacerItem(): if lastItem and lastItem.spacerItem():
self.scrollLayout.insertWidget( self.ScrollLayout.insertWidget(
self.scrollLayout.count() - 1, block self.ScrollLayout.count() - 1, Block
) )
return return
self.scrollLayout.addWidget(block) self.ScrollLayout.addWidget(Block)
def removeBlock( def removeBlock(
self, self,
@@ -119,16 +119,16 @@ class ALAutoScriptOrchDialog(QDialog):
return return
if block in self._blocks: if block in self._blocks:
self._blocks.remove(block) self._blocks.remove(block)
self.scrollLayout.removeWidget(block) self.ScrollLayout.removeWidget(block)
block.hide() block.hide()
block.deleteLater() block.deleteLater()
for i, blk in enumerate(self._blocks): for i, blk in enumerate(self._blocks):
blk.blockIndex = i blk.blockIndex = i
if i == 0: if i == 0:
blk.typeCombo.setEnabled(False) blk.TypeCombo.setEnabled(False)
blk.typeCombo.setCurrentIndex(0) blk.TypeCombo.setCurrentIndex(0)
else: else:
blk.typeCombo.setEnabled(True) blk.TypeCombo.setEnabled(True)
blk.refreshVarCombos() blk.refreshVarCombos()
self.updateBlockTypeRestrictions() self.updateBlockTypeRestrictions()
+68 -68
View File
@@ -110,39 +110,39 @@ class _DateInputContainer(QWidget):
self self
): ):
layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4) Layout.setSpacing(4)
self._modeCombo = QComboBox(self) self._ModeCombo = QComboBox(self)
self._modeCombo.addItem("相对日期", "relative") self._ModeCombo.addItem("相对日期", "relative")
self._modeCombo.addItem("绝对日期", "absolute") self._ModeCombo.addItem("绝对日期", "absolute")
self._modeCombo.setFixedHeight(25) self._ModeCombo.setFixedHeight(25)
self._stack = QStackedWidget(self) self._Stack = QStackedWidget(self)
self._relCombo = QComboBox(self) self._RelCombo = QComboBox(self)
for display, data in DATE_OPTIONS: for display, data in DATE_OPTIONS:
self._relCombo.addItem(display, data) self._RelCombo.addItem(display, data)
self._relCombo.setFixedHeight(25) self._RelCombo.setFixedHeight(25)
self._stack.addWidget(self._relCombo) self._Stack.addWidget(self._RelCombo)
self._dateEdit = QDateEdit(self) self._DateEdit = QDateEdit(self)
self._dateEdit.setDisplayFormat("yyyy-MM-dd") self._DateEdit.setDisplayFormat("yyyy-MM-dd")
self._dateEdit.setCalendarPopup(True) self._DateEdit.setCalendarPopup(True)
self._dateEdit.setFixedHeight(25) self._DateEdit.setFixedHeight(25)
self._stack.addWidget(self._dateEdit) self._Stack.addWidget(self._DateEdit)
self._modeCombo.currentIndexChanged.connect( self._ModeCombo.currentIndexChanged.connect(
lambda i: self._stack.setCurrentIndex(i) lambda i: self._Stack.setCurrentIndex(i)
) )
layout.addWidget(self._modeCombo) Layout.addWidget(self._ModeCombo)
layout.addWidget(self._stack) Layout.addWidget(self._Stack)
layout.addStretch() Layout.addStretch()
def getValue( def getValue(
self self
) -> str: ) -> str:
mode = self._modeCombo.currentData() mode = self._ModeCombo.currentData()
if mode == "relative": if mode == "relative":
return self._relCombo.currentText() return self._RelCombo.currentText()
return self._dateEdit.date().toString("yyyy-MM-dd") return self._DateEdit.date().toString("yyyy-MM-dd")
class _TimeInputContainer(QWidget): class _TimeInputContainer(QWidget):
@@ -153,19 +153,19 @@ class _TimeInputContainer(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self._timeEdit = QTimeEdit(self) self._TimeEdit = QTimeEdit(self)
self._timeEdit.setDisplayFormat("HH:mm") self._TimeEdit.setDisplayFormat("HH:mm")
self._timeEdit.setFixedHeight(25) self._TimeEdit.setFixedHeight(25)
layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._timeEdit) Layout.addWidget(self._TimeEdit)
def getValue( def getValue(
self self
) -> str: ) -> str:
return self._timeEdit.time().toString("HH:mm") return self._TimeEdit.time().toString("HH:mm")
class _DateOffsetContainer(QWidget): class _DateOffsetContainer(QWidget):
@@ -176,20 +176,20 @@ class _DateOffsetContainer(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self._spinBox = QSpinBox(self) self._SpinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999) self._SpinBox.setRange(0, 99999)
self._spinBox.setFixedHeight(25) self._SpinBox.setFixedHeight(25)
self._unitCombo = QComboBox(self) self._UnitCombo = QComboBox(self)
for display, data in DATE_OFFSET_OPTIONS: for display, data in DATE_OFFSET_OPTIONS:
self._unitCombo.addItem(display, data) self._UnitCombo.addItem(display, data)
self._unitCombo.setFixedHeight(25) self._UnitCombo.setFixedHeight(25)
layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4) Layout.setSpacing(4)
layout.addWidget(self._spinBox) Layout.addWidget(self._SpinBox)
layout.addWidget(self._unitCombo) Layout.addWidget(self._UnitCombo)
layout.addStretch() Layout.addStretch()
def getValue( def getValue(
self self
@@ -201,8 +201,8 @@ class _DateOffsetContainer(QWidget):
self self
) -> int: ) -> int:
val = self._spinBox.value() val = self._SpinBox.value()
unit = self._unitCombo.currentData() unit = self._UnitCombo.currentData()
if unit == "weeks": if unit == "weeks":
return val*7 return val*7
if unit == "months": if unit == "months":
@@ -220,14 +220,14 @@ class _TimeOffsetContainer(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self._spinBox = QSpinBox(self) self._SpinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999) self._SpinBox.setRange(0, 99999)
self._spinBox.setSuffix(" 小时") self._SpinBox.setSuffix(" 小时")
self._spinBox.setFixedHeight(25) self._SpinBox.setFixedHeight(25)
layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) Layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._spinBox) Layout.addWidget(self._SpinBox)
def getValue( def getValue(
self self
@@ -239,7 +239,7 @@ class _TimeOffsetContainer(QWidget):
self self
) -> int: ) -> int:
return self._spinBox.value() return self._SpinBox.value()
class VariableManager(QObject): class VariableManager(QObject):
@@ -364,11 +364,11 @@ def makeVarRefCombo(
parent: QWidget = None parent: QWidget = None
) -> QComboBox: ) -> QComboBox:
cb = QComboBox(parent) Cb = QComboBox(parent)
cb.setFixedHeight(25) Cb.setFixedHeight(25)
cb.setMinimumWidth(120) Cb.setMinimumWidth(120)
cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) Cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return cb return Cb
def makeComboWidget( def makeComboWidget(
items, items,
@@ -376,12 +376,12 @@ def makeComboWidget(
parent: QWidget = None parent: QWidget = None
) -> QComboBox: ) -> QComboBox:
cb = QComboBox(parent) Cb = QComboBox(parent)
for display, data in items: for display, data in items:
cb.addItem(display, data) Cb.addItem(display, data)
cb.setFixedHeight(25) Cb.setFixedHeight(25)
cb.setMinimumWidth(min_width) Cb.setMinimumWidth(min_width)
return cb return Cb
def makeLabel( def makeLabel(
text: str, text: str,
@@ -389,11 +389,11 @@ def makeLabel(
width: int = None width: int = None
) -> QLabel: ) -> QLabel:
lbl = QLabel(text, parent) Lbl = QLabel(text, parent)
lbl.setFixedHeight(25) Lbl.setFixedHeight(25)
if width: if width:
lbl.setFixedWidth(width) Lbl.setFixedWidth(width)
return lbl return Lbl
def getValueFromWidget( def getValueFromWidget(
w: QWidget w: QWidget
+124 -124
View File
@@ -66,42 +66,42 @@ class ConditionRowFrame(QFrame):
self.setFrameShape(QFrame.StyledPanel) self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised) self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(32) self.setFixedHeight(32)
layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2) Layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4) Layout.setSpacing(4)
if self._isFirst: if self._isFirst:
self.logicCombo = None self.LogicCombo = None
else: else:
self.logicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self) self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
layout.addWidget(self.logicCombo) Layout.addWidget(self.LogicCombo)
self.leftVarCombo = QComboBox(self) self.LeftVarCombo = QComboBox(self)
self.leftVarCombo.setFixedHeight(25) self.LeftVarCombo.setFixedHeight(25)
self.leftVarCombo.setMinimumWidth(120) self.LeftVarCombo.setMinimumWidth(120)
self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.LeftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.populateLeftVarCombo() self.populateLeftVarCombo()
layout.addWidget(self.leftVarCombo) Layout.addWidget(self.LeftVarCombo)
self.opCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self) self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
layout.addWidget(self.opCombo) Layout.addWidget(self.OpCombo)
self._compTypeCombo = makeComboWidget([ self._CompTypeCombo = makeComboWidget([
("特定值", "literal"), ("特定值", "literal"),
("变量", "variable"), ("变量", "variable"),
], min_width=70, parent=self) ], min_width=70, parent=self)
layout.addWidget(self._compTypeCombo) Layout.addWidget(self._CompTypeCombo)
self.rhsStack = QStackedWidget(self) self.RhsStack = QStackedWidget(self)
self.rhsStack.setFixedHeight(25) self.RhsStack.setFixedHeight(25)
self.initLiteralStack() self.initLiteralStack()
self.rhsVarCombo = makeVarRefCombo(self) self.RhsVarCombo = makeVarRefCombo(self)
self.rhsStack.addWidget(self.rhsVarCombo) self.RhsStack.addWidget(self.RhsVarCombo)
self.rhsStack.setCurrentIndex(0) self.RhsStack.setCurrentIndex(0)
layout.addWidget(self.rhsStack) Layout.addWidget(self.RhsStack)
if not self._isFirst: if not self._isFirst:
self.deleteBtn = QPushButton("×", self) self.DeleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25) self.DeleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;") self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn) Layout.addWidget(self.DeleteBtn)
else: else:
self.deleteBtn = None self.DeleteBtn = None
layout.addStretch() Layout.addStretch()
self.setUpdatesEnabled(True) self.setUpdatesEnabled(True)
def populateLeftVarCombo( def populateLeftVarCombo(
@@ -111,53 +111,53 @@ class ConditionRowFrame(QFrame):
wasBool = self._isBoolMode wasBool = self._isBoolMode
boolName = None boolName = None
if wasBool: if wasBool:
data = self.leftVarCombo.currentData() data = self.LeftVarCombo.currentData()
if data: if data:
boolName = data[0] boolName = data[0]
self._varMgr.populateCombo(self.leftVarCombo) self._varMgr.populateCombo(self.LeftVarCombo)
# Append boolean literal sentinels at the end # Append boolean literal sentinels at the end
self.leftVarCombo.insertSeparator(self.leftVarCombo.count()) self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count())
self.leftVarCombo.addItem("true", ("true", "Boolean")) self.LeftVarCombo.addItem("true", ("true", "Boolean"))
self.leftVarCombo.addItem("false", ("false", "Boolean")) self.LeftVarCombo.addItem("false", ("false", "Boolean"))
if wasBool and boolName: if wasBool and boolName:
for ci in range(self.leftVarCombo.count()): for ci in range(self.LeftVarCombo.count()):
d = self.leftVarCombo.itemData(ci) d = self.LeftVarCombo.itemData(ci)
if d and d[0] == boolName: if d and d[0] == boolName:
self.leftVarCombo.setCurrentIndex(ci) self.LeftVarCombo.setCurrentIndex(ci)
break break
def populateRHSVarCombo( def populateRHSVarCombo(
self self
): ):
self._varMgr.populateCombo(self.rhsVarCombo) self._varMgr.populateCombo(self.RhsVarCombo)
def initLiteralStack( def initLiteralStack(
self self
): ):
self.literalStack = QStackedWidget(self) self.LiteralStack = QStackedWidget(self)
self.literalStack.setFixedHeight(25) self.LiteralStack.setFixedHeight(25)
self._literalWidgets = {} self._literalWidgets = {}
for vt in getTypeOrder(): for vt in getTypeOrder():
w = makeValueWidget(vt, self.literalStack) W = makeValueWidget(vt, self.LiteralStack)
self._literalWidgets[vt] = w self._literalWidgets[vt] = W
self.literalStack.addWidget(w) self.LiteralStack.addWidget(W)
self.literalStack.setCurrentWidget(self._literalWidgets.get("String")) self.LiteralStack.setCurrentWidget(self._literalWidgets.get("String"))
self.rhsStack.addWidget(self.literalStack) self.RhsStack.addWidget(self.LiteralStack)
def connectSignals( def connectSignals(
self self
): ):
self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) self._CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
def getLogic( def getLogic(
self self
) -> str: ) -> str:
return self.logicCombo.currentData() if self.logicCombo else "" return self.LogicCombo.currentData() if self.LogicCombo else ""
def updateRHSLiteralWidget( def updateRHSLiteralWidget(
self, self,
@@ -166,13 +166,13 @@ class ConditionRowFrame(QFrame):
if vartype not in self._literalWidgets: if vartype not in self._literalWidgets:
vartype = "String" vartype = "String"
self.literalStack.setCurrentWidget(self._literalWidgets[vartype]) self.LiteralStack.setCurrentWidget(self._literalWidgets[vartype])
def toScript( def toScript(
self self
) -> str: ) -> str:
data = self.leftVarCombo.currentData() data = self.LeftVarCombo.currentData()
if self._isBoolMode and data: if self._isBoolMode and data:
return data[0] return data[0]
if not data: if not data:
@@ -183,12 +183,12 @@ class ConditionRowFrame(QFrame):
name = "datenow()" name = "datenow()"
elif name == "CURRENT_TIME": elif name == "CURRENT_TIME":
name = "timenow()" name = "timenow()"
opSym = self.opCombo.currentData() opSym = self.OpCombo.currentData()
if self._rawRhsExpr: if self._rawRhsExpr:
return f"{name} {opSym} {self._rawRhsExpr}" return f"{name} {opSym} {self._rawRhsExpr}"
isVarRef = (self._compTypeCombo.currentData() == "variable") isVarRef = (self._CompTypeCombo.currentData() == "variable")
if isVarRef: if isVarRef:
rd = self.rhsVarCombo.currentData() rd = self.RhsVarCombo.currentData()
if rd: if rd:
rhsName = rd[0] rhsName = rd[0]
if rhsName == "CURRENT_DATE": if rhsName == "CURRENT_DATE":
@@ -196,7 +196,7 @@ class ConditionRowFrame(QFrame):
elif rhsName == "CURRENT_TIME": elif rhsName == "CURRENT_TIME":
rhsName = "timenow()" rhsName = "timenow()"
return f"{name} {opSym} {rhsName}" return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip() rhsText = self.RhsVarCombo.currentText().strip()
if rhsText: if rhsText:
return f"{name} {opSym} {rhsText}" return f"{name} {opSym} {rhsText}"
return "" return ""
@@ -223,15 +223,15 @@ class ConditionRowFrame(QFrame):
self._rawRhsExpr = "" self._rawRhsExpr = ""
if idx < 0: if idx < 0:
return return
data = self.leftVarCombo.itemData(idx) data = self.LeftVarCombo.itemData(idx)
if not data: if not data:
return return
name, vartype = data name, vartype = data
isBool = name in ("true", "false") isBool = name in ("true", "false")
self._isBoolMode = isBool self._isBoolMode = isBool
self.opCombo.setVisible(not isBool) self.OpCombo.setVisible(not isBool)
self._compTypeCombo.setVisible(not isBool) self._CompTypeCombo.setVisible(not isBool)
self.rhsStack.setVisible(not isBool) self.RhsStack.setVisible(not isBool)
if not isBool: if not isBool:
self.updateRHSLiteralWidget(vartype) self.updateRHSLiteralWidget(vartype)
@@ -242,8 +242,8 @@ class ConditionRowFrame(QFrame):
): ):
self._rawRhsExpr = "" self._rawRhsExpr = ""
isVar = (self._compTypeCombo.currentData() == "variable") isVar = (self._CompTypeCombo.currentData() == "variable")
self.rhsStack.setCurrentIndex(1 if isVar else 0) self.RhsStack.setCurrentIndex(1 if isVar else 0)
if isVar: if isVar:
self.populateRHSVarCombo() self.populateRHSVarCombo()
@@ -273,52 +273,52 @@ class ActionStepFrame(QFrame):
self.setFrameShape(QFrame.StyledPanel) self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised) self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(35) self.setFixedHeight(35)
layout = QHBoxLayout(self) Layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2) Layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4) Layout.setSpacing(4)
self.opTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self) self.OpTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self)
layout.addWidget(self.opTypeCombo) Layout.addWidget(self.OpTypeCombo)
layout.addWidget(makeLabel("设置", self)) Layout.addWidget(makeLabel("设置", self))
self.targetCombo = QComboBox(self) self.TargetCombo = QComboBox(self)
self.targetCombo.setFixedHeight(25) self.TargetCombo.setFixedHeight(25)
self.targetCombo.setMinimumWidth(120) self.TargetCombo.setMinimumWidth(120)
self.populateTargetCombo() self.populateTargetCombo()
layout.addWidget(self.targetCombo) Layout.addWidget(self.TargetCombo)
layout.addWidget(makeLabel("", self)) Layout.addWidget(makeLabel("", self))
self.valueSrcCombo = makeComboWidget([ self.ValueSrcCombo = makeComboWidget([
("特定值", "literal"), ("特定值", "literal"),
("变量", "variable"), ("变量", "variable"),
], min_width=70, parent=self) ], min_width=70, parent=self)
layout.addWidget(self.valueSrcCombo) Layout.addWidget(self.ValueSrcCombo)
self.valueStack = QStackedWidget(self) self.ValueStack = QStackedWidget(self)
self.valueStack.setFixedHeight(25) self.ValueStack.setFixedHeight(25)
self.initValueStacks() self.initValueStacks()
layout.addWidget(self.valueStack) Layout.addWidget(self.ValueStack)
self.existingVarCombo = makeVarRefCombo(self) self.ExistingVarCombo = makeVarRefCombo(self)
self.existingVarCombo.setVisible(False) self.ExistingVarCombo.setVisible(False)
layout.addWidget(self.existingVarCombo) Layout.addWidget(self.ExistingVarCombo)
self.deleteBtn = QPushButton("×", self) self.DeleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25) self.DeleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;") self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn) Layout.addWidget(self.DeleteBtn)
self.setUpdatesEnabled(True) self.setUpdatesEnabled(True)
def populateTargetCombo( def populateTargetCombo(
self self
): ):
self.targetCombo.blockSignals(True) self.TargetCombo.blockSignals(True)
self.targetCombo.clear() self.TargetCombo.clear()
for p in getPresetVars(): for p in getPresetVars():
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"): if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue continue
info = self._varMgr.getInfoByName(p["name"]) info = self._varMgr.getInfoByName(p["name"])
if info: if info:
self.targetCombo.addItem( self.TargetCombo.addItem(
info["display"], info["display"],
(info["name"], info["type"]) (info["name"], info["type"])
) )
self.targetCombo.blockSignals(False) self.TargetCombo.blockSignals(False)
def initValueStacks( def initValueStacks(
self self
@@ -327,45 +327,45 @@ class ActionStepFrame(QFrame):
self._literalWidgets = {} self._literalWidgets = {}
self._offsetWidgets = {} self._offsetWidgets = {}
for vt in getTypeOrder(): for vt in getTypeOrder():
self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack) self._literalWidgets[vt] = makeValueWidget(vt, self.ValueStack)
self.valueStack.addWidget(self._literalWidgets[vt]) self.ValueStack.addWidget(self._literalWidgets[vt])
if getArithType(vt): if getArithType(vt):
self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack) self._offsetWidgets[vt] = makeOffsetWidget(vt, self.ValueStack)
self.valueStack.addWidget(self._offsetWidgets[vt]) self.ValueStack.addWidget(self._offsetWidgets[vt])
else: else:
lbl = QLabel("(不支持该操作)", self.valueStack) Lbl = QLabel("(不支持该操作)", self.ValueStack)
lbl.setFixedHeight(25) Lbl.setFixedHeight(25)
self._offsetWidgets[vt] = lbl self._offsetWidgets[vt] = Lbl
self.valueStack.addWidget(lbl) self.ValueStack.addWidget(Lbl)
def connectSignals( def connectSignals(
self self
): ):
self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged) self.OpTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged) self.TargetCombo.currentIndexChanged.connect(self.onTargetChanged)
self.valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged) self.ValueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
def getTargetName( def getTargetName(
self self
) -> str: ) -> str:
data = self.targetCombo.currentData() data = self.TargetCombo.currentData()
return data[0] if data else "" return data[0] if data else ""
def updateValueWidget( def updateValueWidget(
self self
): ):
op = self.opTypeCombo.currentData() op = self.OpTypeCombo.currentData()
isArith = (op in ("add", "sub")) isArith = (op in ("add", "sub"))
actualType = self._currentTargetType actualType = self._currentTargetType
if isArith and actualType in self._offsetWidgets: if isArith and actualType in self._offsetWidgets:
self.valueStack.setCurrentWidget(self._offsetWidgets[actualType]) self.ValueStack.setCurrentWidget(self._offsetWidgets[actualType])
elif actualType in self._literalWidgets: elif actualType in self._literalWidgets:
self.valueStack.setCurrentWidget(self._literalWidgets[actualType]) self.ValueStack.setCurrentWidget(self._literalWidgets[actualType])
else: else:
self.valueStack.setCurrentWidget(self._literalWidgets.get("String")) self.ValueStack.setCurrentWidget(self._literalWidgets.get("String"))
def toScript( def toScript(
self self
@@ -375,7 +375,7 @@ class ActionStepFrame(QFrame):
""" """
target = self.getTargetName() target = self.getTargetName()
op = self.opTypeCombo.currentData() op = self.OpTypeCombo.currentData()
if op == "pass": if op == "pass":
return " -- pass" return " -- pass"
if not target: if not target:
@@ -386,19 +386,19 @@ class ActionStepFrame(QFrame):
encoded = encodeValueStr(rawVal, vartype) encoded = encodeValueStr(rawVal, vartype)
return f" {target} = {encoded}" return f" {target} = {encoded}"
elif op == "add": elif op == "add":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays() days = self.ValueStack.currentWidget().getOffsetDays()
return f" {target} = dateadd({target}, {days})" return f" {target} = dateadd({target}, {days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours() hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, {hours})" return f" {target} = timeadd({target}, {hours})"
return f" {target} = {target} + {rawVal}" return f" {target} = {target} + {rawVal}"
elif op == "sub": elif op == "sub":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays() days = self.ValueStack.currentWidget().getOffsetDays()
return f" {target} = dateadd({target}, -{days})" return f" {target} = dateadd({target}, -{days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours() hours = self.ValueStack.currentWidget().getOffsetHours()
return f" {target} = timeadd({target}, -{hours})" return f" {target} = timeadd({target}, -{hours})"
return f" {target} = {target} - {rawVal}" return f" {target} = {target} - {rawVal}"
return "" return ""
@@ -407,10 +407,10 @@ class ActionStepFrame(QFrame):
self self
) -> str: ) -> str:
if self.valueSrcCombo.currentData() == "variable": if self.ValueSrcCombo.currentData() == "variable":
data = self.existingVarCombo.currentData() data = self.ExistingVarCombo.currentData()
return data[0] if data else "" return data[0] if data else ""
w = self.valueStack.currentWidget() w = self.ValueStack.currentWidget()
if w: if w:
return getValueFromWidget(w) return getValueFromWidget(w)
return "" return ""
@@ -419,15 +419,15 @@ class ActionStepFrame(QFrame):
self self
): ):
currentData = self.targetCombo.currentData() currentData = self.TargetCombo.currentData()
self.populateTargetCombo() self.populateTargetCombo()
if currentData: if currentData:
for i in range(self.targetCombo.count()): for i in range(self.TargetCombo.count()):
d = self.targetCombo.itemData(i) d = self.TargetCombo.itemData(i)
if d and d[0] == currentData[0]: if d and d[0] == currentData[0]:
self.targetCombo.setCurrentIndex(i) self.TargetCombo.setCurrentIndex(i)
break break
self._varMgr.populateCombo(self.existingVarCombo) self._varMgr.populateCombo(self.ExistingVarCombo)
@Slot(int) @Slot(int)
def onTargetChanged( def onTargetChanged(
@@ -437,13 +437,13 @@ class ActionStepFrame(QFrame):
if idx < 0: if idx < 0:
return return
data = self.targetCombo.itemData(idx) data = self.TargetCombo.itemData(idx)
if not data: if not data:
return return
_, vartype = data _, vartype = data
self._currentTargetType = vartype self._currentTargetType = vartype
self.updateValueWidget() self.updateValueWidget()
self.onValueSrcChanged(self.valueSrcCombo.currentIndex()) self.onValueSrcChanged(self.ValueSrcCombo.currentIndex())
@Slot(int) @Slot(int)
def onOpTypeChanged( def onOpTypeChanged(
@@ -459,10 +459,10 @@ class ActionStepFrame(QFrame):
idx idx
): ):
isVar = (self.valueSrcCombo.currentData() == "variable") isVar = (self.ValueSrcCombo.currentData() == "variable")
self.valueStack.setVisible(not isVar) self.ValueStack.setVisible(not isVar)
self.existingVarCombo.setVisible(isVar) self.ExistingVarCombo.setVisible(isVar)
if isVar: if isVar:
self._varMgr.populateCombo(self.existingVarCombo) self._varMgr.populateCombo(self.ExistingVarCombo)
else: else:
self.updateValueWidget() self.updateValueWidget()
+100 -100
View File
@@ -386,18 +386,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config = self.defaultUserConfig() user_config = self.defaultUserConfig()
for i in range(self.UserTreeWidget.topLevelItemCount()): for i in range(self.UserTreeWidget.topLevelItemCount()):
group_item = self.UserTreeWidget.topLevelItem(i) GroupItem = self.UserTreeWidget.topLevelItem(i)
group_config = { group_config = {
"name": group_item.text(0), "name": GroupItem.text(0),
"enabled": group_item.checkState(1) == Qt.CheckState.Checked, "enabled": GroupItem.checkState(1) == Qt.CheckState.Checked,
"users": [] "users": []
} }
for j in range(group_item.childCount()): for j in range(GroupItem.childCount()):
user_item = group_item.child(j) UserItem = GroupItem.child(j)
user = user_item.data(0, Qt.UserRole) user = UserItem.data(0, Qt.UserRole)
if not user: if not user:
continue continue
user["enabled"] = user_item.checkState(1) == Qt.CheckState.Checked user["enabled"] = UserItem.checkState(1) == Qt.CheckState.Checked
group_config["users"].append(user) group_config["users"].append(user)
user_config["groups"].append(group_config) user_config["groups"].append(group_config)
return user_config return user_config
@@ -453,18 +453,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
try: try:
if "groups" in users: if "groups" in users:
for group_config in users["groups"]: for group_config in users["groups"]:
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
group_item.setText(0, group_config["name"]) GroupItem.setText(0, group_config["name"])
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable) GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked) GroupItem.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
for user_config in group_config["users"]: for user_config in group_config["users"]:
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value) UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value)
user_item.setText(0, user_config["username"]) UserItem.setText(0, user_config["username"])
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过") UserItem.setText(1, "" if user_config.get("enabled", True) else "跳过")
user_item.setData(0, Qt.UserRole, user_config) UserItem.setData(0, Qt.UserRole, user_config)
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked) UserItem.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
user_item.setDisabled(not group_config.get("enabled", True)) UserItem.setDisabled(not group_config.get("enabled", True))
group_item.setExpanded(True) GroupItem.setExpanded(True)
except KeyError as e: except KeyError as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -638,43 +638,43 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> QTreeWidgetItem: ) -> QTreeWidgetItem:
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) 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: if not group_name:
group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}" group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}"
group_item.setText(0, group_name) GroupItem.setText(0, group_name)
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable) GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked) GroupItem.setCheckState(1, Qt.Checked)
self.UserTreeWidget.setCurrentItem(group_item) self.UserTreeWidget.setCurrentItem(GroupItem)
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return group_item return GroupItem
def delGroup( def delGroup(
self, self,
group_item: QTreeWidgetItem = None GroupItem: QTreeWidgetItem = None
): ):
if group_item is None: if GroupItem is None:
return return
if group_item.type() != ALUserTreeItemType.GROUP.value: if GroupItem.type() != ALUserTreeItemType.GROUP.value:
return return
index = self.UserTreeWidget.indexOfTopLevelItem(group_item) index = self.UserTreeWidget.indexOfTopLevelItem(GroupItem)
self.UserTreeWidget.takeTopLevelItem(index) self.UserTreeWidget.takeTopLevelItem(index)
def addUser( def addUser(
self, self,
group_item: QTreeWidgetItem = None GroupItem: QTreeWidgetItem = None
) -> QTreeWidgetItem: ) -> QTreeWidgetItem:
if group_item is None: if GroupItem is None:
current_item = self.UserTreeWidget.currentItem() CurrentItem = self.UserTreeWidget.currentItem()
if current_item is None: if CurrentItem is None:
group_item = self.addGroup() GroupItem = self.addGroup()
if group_item.type() == ALUserTreeItemType.USER.value: if GroupItem.type() == ALUserTreeItemType.USER.value:
group_item = group_item.parent() GroupItem = GroupItem.parent()
if group_item.checkState(1) == Qt.CheckState.Unchecked: if GroupItem.checkState(1) == Qt.CheckState.Unchecked:
return None return None
new_user = { new_user = {
"username": f"新用户-{group_item.childCount()}", "username": f"新用户-{GroupItem.childCount()}",
"password": "000000", "password": "000000",
"enabled": True, "enabled": True,
"reserve_info": { "reserve_info": {
@@ -703,30 +703,30 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
} }
} }
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value) UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value)
user_item.setText(0, new_user["username"]) UserItem.setText(0, new_user["username"])
user_item.setText(1, "") UserItem.setText(1, "")
user_item.setData(0, Qt.UserRole, new_user) UserItem.setData(0, Qt.UserRole, new_user)
user_item.setCheckState(1, Qt.CheckState.Checked) UserItem.setCheckState(1, Qt.CheckState.Checked)
group_item.setExpanded(True) GroupItem.setExpanded(True)
self.UserTreeWidget.setCurrentItem(user_item) self.UserTreeWidget.setCurrentItem(UserItem)
self.setUserToWidget(new_user) self.setUserToWidget(new_user)
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return user_item return UserItem
def delUser( def delUser(
self, self,
user_item: QTreeWidgetItem = None UserItem: QTreeWidgetItem = None
): ):
if user_item is None: if UserItem is None:
return return
if user_item.type() != ALUserTreeItemType.USER.value: if UserItem.type() != ALUserTreeItemType.USER.value:
return return
parent_item = user_item.parent() ParentItem = UserItem.parent()
index = parent_item.indexOfChild(user_item) index = ParentItem.indexOfChild(UserItem)
parent_item.takeChild(index) ParentItem.takeChild(index)
if parent_item.childCount() == 0: if ParentItem.childCount() == 0:
self.UserTreeWidget.setCurrentItem(None) self.UserTreeWidget.setCurrentItem(None)
def renameItem( def renameItem(
@@ -787,19 +787,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
room = self.RoomComboBox.currentText() room = self.RoomComboBox.currentText()
floor_idx = self.__floor_rmap[floor] floor_idx = self.__floor_rmap[floor]
room_idx = self.__room_rmap[room] room_idx = self.__room_rmap[room]
dialog = ALSeatMapSelectDialog( Dialog = ALSeatMapSelectDialog(
self, self,
floor, floor,
room, room,
ALSeatMapTable[floor_idx][room_idx] ALSeatMapTable[floor_idx][room_idx]
) )
dialog.selectSeats(self.SeatIDEdit.text().split(",")) Dialog.selectSeats(self.SeatIDEdit.text().split(","))
if dialog.exec() == QDialog.DialogCode.Accepted: if Dialog.exec() == QDialog.DialogCode.Accepted:
selected_seats = dialog.getSelectedSeats() selected_seats = Dialog.getSelectedSeats()
if len(selected_seats) == 0: if len(selected_seats) == 0:
self.SeatIDEdit.clear() self.SeatIDEdit.clear()
return return
self.SeatIDEdit.setText(",".join(dialog.getSelectedSeats())) self.SeatIDEdit.setText(",".join(Dialog.getSelectedSeats()))
@Slot() @Slot()
def onUserTreeWidgetCurrentItemChanged( def onUserTreeWidgetCurrentItemChanged(
@@ -844,10 +844,10 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if item.type() == ALUserTreeItemType.GROUP.value: if item.type() == ALUserTreeItemType.GROUP.value:
is_checked = item.checkState(1) == Qt.CheckState.Checked is_checked = item.checkState(1) == Qt.CheckState.Checked
for i in range(item.childCount()): for i in range(item.childCount()):
child = item.child(i) Child = item.child(i)
if self.UserTreeWidget.currentItem() == child: if self.UserTreeWidget.currentItem() == Child:
self.UserTreeWidget.setCurrentItem(item) self.UserTreeWidget.setCurrentItem(item)
child.setDisabled(not is_checked) Child.setDisabled(not is_checked)
else: else:
is_checked = item.checkState(1) == Qt.CheckState.Checked is_checked = item.checkState(1) == Qt.CheckState.Checked
item.setText(1, "" if is_checked else "跳过") item.setText(1, "" if is_checked else "跳过")
@@ -857,41 +857,41 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
menu: QMenu menu: QMenu
): ):
add_group_action = QAction("添加分组", menu) AddGroupAction = QAction("添加分组", menu)
add_group_action.triggered.connect(self.addGroup) AddGroupAction.triggered.connect(self.addGroup)
menu.addAction(add_group_action) menu.addAction(AddGroupAction)
def showGroupMenu( def showGroupMenu(
self, self,
menu: QMenu, menu: QMenu,
group_item: QTreeWidgetItem = None GroupItem: QTreeWidgetItem = None
): ):
add_user_action = QAction("添加用户", menu) AddUserAction = QAction("添加用户", menu)
rename_group_action = QAction("重命名分组", menu) RenameGroupAction = QAction("重命名分组", menu)
del_group_action = QAction("删除分组", menu) DelGroupAction = QAction("删除分组", menu)
add_user_action.triggered.connect(lambda: self.addUser(group_item)) AddUserAction.triggered.connect(lambda: self.addUser(GroupItem))
rename_group_action.triggered.connect(lambda: self.renameItem(group_item)) RenameGroupAction.triggered.connect(lambda: self.renameItem(GroupItem))
del_group_action.triggered.connect(lambda: self.delGroup(group_item)) DelGroupAction.triggered.connect(lambda: self.delGroup(GroupItem))
menu.addAction(add_user_action) menu.addAction(AddUserAction)
menu.addSeparator() menu.addSeparator()
menu.addAction(rename_group_action) menu.addAction(RenameGroupAction)
menu.addAction(del_group_action) menu.addAction(DelGroupAction)
if group_item.checkState(1) == Qt.CheckState.Unchecked: if GroupItem.checkState(1) == Qt.CheckState.Unchecked:
add_user_action.setEnabled(False) AddUserAction.setEnabled(False)
def showUserMenu( def showUserMenu(
self, self,
menu: QMenu, menu: QMenu,
user_item: QTreeWidgetItem = None UserItem: QTreeWidgetItem = None
): ):
rename_user_action = QAction("重命名用户", menu) RenameUserAction = QAction("重命名用户", menu)
del_user_action = QAction("删除用户", menu) DelUserAction = QAction("删除用户", menu)
rename_user_action.triggered.connect(lambda: self.renameItem(user_item)) RenameUserAction.triggered.connect(lambda: self.renameItem(UserItem))
del_user_action.triggered.connect(lambda: self.delUser(user_item)) DelUserAction.triggered.connect(lambda: self.delUser(UserItem))
menu.addAction(rename_user_action) menu.addAction(RenameUserAction)
menu.addAction(del_user_action) menu.addAction(DelUserAction)
@Slot() @Slot()
def onUserTreeWidgetContextMenu( def onUserTreeWidgetContextMenu(
@@ -899,31 +899,31 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
pos pos
): ):
current_item = self.UserTreeWidget.itemAt(pos) CurrentItem = self.UserTreeWidget.itemAt(pos)
menu = QMenu(self.UserTreeWidget) Menu = QMenu(self.UserTreeWidget)
if current_item is None: if CurrentItem is None:
self.showTreeMenu(menu) self.showTreeMenu(Menu)
elif current_item.type() == ALUserTreeItemType.GROUP.value: elif CurrentItem.type() == ALUserTreeItemType.GROUP.value:
self.showGroupMenu(menu, current_item) self.showGroupMenu(Menu, CurrentItem)
else: else:
self.showUserMenu(menu, current_item) self.showUserMenu(Menu, CurrentItem)
menu.exec_(self.UserTreeWidget.mapToGlobal(pos)) Menu.exec_(self.UserTreeWidget.mapToGlobal(pos))
@Slot() @Slot()
def onAddUserButtonClicked( def onAddUserButtonClicked(
self self
): ):
current_item = self.UserTreeWidget.currentItem() CurrentItem = self.UserTreeWidget.currentItem()
self.addUser(current_item) self.addUser(CurrentItem)
@Slot() @Slot()
def onDelUserButtonClicked( def onDelUserButtonClicked(
self self
): ):
current_item = self.UserTreeWidget.currentItem() CurrentItem = self.UserTreeWidget.currentItem()
self.delUser(current_item) self.delUser(CurrentItem)
@Slot() @Slot()
def onBrowseBrowserDriverButtonClicked( def onBrowseBrowserDriverButtonClicked(
@@ -944,10 +944,10 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self self
): ):
dialog = ALWebDriverDownloadDialog(self) Dialog = ALWebDriverDownloadDialog(self)
dialog.show() Dialog.show()
dialog.exec_() Dialog.exec_()
selected_driver_info = dialog.getSelectedDriverInfo() selected_driver_info = Dialog.getSelectedDriverInfo()
if selected_driver_info and selected_driver_info.driver_path: if selected_driver_info and selected_driver_info.driver_path:
self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value) self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path))) self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
@@ -1133,8 +1133,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self self
): ):
current_item = self.UserTreeWidget.currentItem() CurrentItem = self.UserTreeWidget.currentItem()
if current_item and current_item.type() == ALUserTreeItemType.USER.value: if CurrentItem and CurrentItem.type() == ALUserTreeItemType.USER.value:
self.UserTreeWidget.setCurrentItem(None) self.UserTreeWidget.setCurrentItem(None)
if self.saveConfigs( if self.saveConfigs(
self.__config_paths["run"], self.__config_paths["run"],
+7 -7
View File
@@ -76,8 +76,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
self.icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg") self.Icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg")
self.setWindowIcon(self.icon) self.setWindowIcon(self.Icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10)) self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered) self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered) self.AboutAction.triggered.connect(self.onAboutActionTriggered)
@@ -106,15 +106,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self self
): ):
about_dialog = ALAboutDialog(self) AboutDialog = ALAboutDialog(self)
about_dialog.exec() AboutDialog.exec()
def onManualActionTriggered( def onManualActionTriggered(
self self
): ):
url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") Url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(url) QDesktopServices.openUrl(Url)
def setupTray( def setupTray(
self self
@@ -123,7 +123,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if not QSystemTrayIcon.isSystemTrayAvailable(): if not QSystemTrayIcon.isSystemTrayAvailable():
self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING) self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING)
return return
self.TrayIcon = QSystemTrayIcon(self.icon, self) self.TrayIcon = QSystemTrayIcon(self.Icon, self)
self.TrayIcon.setToolTip("AutoLibrary") self.TrayIcon.setToolTip("AutoLibrary")
self.TrayMenu = QMenu() self.TrayMenu = QMenu()
+8 -8
View File
@@ -103,15 +103,15 @@ class ALSeatMapView(QGraphicsView):
seats_number = [seat.strip() for seat in row.split(",")] seats_number = [seat.strip() for seat in row.split(",")]
for seat_number in seats_number: for seat_number in seats_number:
if seat_number: if seat_number:
seat_widget = ALSeatFrame(seat_number) SeatWidget = ALSeatFrame(seat_number)
seat_widget.clicked.connect(self.onSeatClicked) SeatWidget.clicked.connect(self.onSeatClicked)
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx) self.SeatsContainerLayout.addWidget(SeatWidget, row_idx, col_idx)
self.__seat_frames[seat_number] = seat_widget self.__seat_frames[seat_number] = SeatWidget
else: else:
spacer = QFrame() Spacer = QFrame()
spacer.setFixedSize(20, 30) Spacer.setFixedSize(20, 30)
spacer.setStyleSheet("background-color: transparent; border: none;") Spacer.setStyleSheet("background-color: transparent; border: none;")
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx) self.SeatsContainerLayout.addWidget(Spacer, row_idx, col_idx)
col_idx += 1 col_idx += 1
self.SeatsContainerLayout.setSpacing(20) self.SeatsContainerLayout.setSpacing(20)
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20) self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
+67 -68
View File
@@ -56,7 +56,6 @@ class ALStatusLabel(QLabel):
self.setFixedSize(36, 36) self.setFixedSize(36, 36)
self.setAlignment(Qt.AlignmentFlag.AlignCenter) self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.RunningAnimation = QPropertyAnimation(self, b"iconAngle") self.RunningAnimation = QPropertyAnimation(self, b"iconAngle")
self.RunningAnimation.setDuration(1000) self.RunningAnimation.setDuration(1000)
self.RunningAnimation.setStartValue(0) self.RunningAnimation.setStartValue(0)
@@ -119,35 +118,35 @@ class ALStatusLabel(QLabel):
event event
): ):
painter = QPainter(self) Painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing) Painter.setRenderHint(QPainter.RenderHint.Antialiasing)
center_x = self.width()/2 center_x = self.width()/2
center_y = self.height()/2 center_y = self.height()/2
radius = min(center_x, center_y) - 3 radius = min(center_x, center_y) - 3
match self.__status: match self.__status:
case self.Status.WAITING: case self.Status.WAITING:
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(2) Pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush) Pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#969696")) # grey Pen.setColor(QColor("#969696")) # grey
painter.setPen(pen) Painter.setPen(Pen)
painter.drawEllipse( Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
case self.Status.RUNNING: case self.Status.RUNNING:
gradient = QConicalGradient(center_x, center_y, self.__icon_angle) Gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF")) Gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
gradient.setColorAt(1.0, QColor("#2294FF00")) Gradient.setColorAt(1.0, QColor("#2294FF00"))
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(3) Pen.setWidth(3)
pen.setBrush(gradient) Pen.setBrush(Gradient)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(pen) Painter.setPen(Pen)
painter.drawEllipse( Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
@@ -155,102 +154,102 @@ class ALStatusLabel(QLabel):
) )
case self.Status.SUCCESS: case self.Status.SUCCESS:
# draw the success green circle # draw the success green circle
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(2) Pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush) Pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green Pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
painter.setPen(pen) Painter.setPen(Pen)
painter.drawEllipse( Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
# draw the success check mark '✓' # draw the success check mark '✓'
painter.setPen(Qt.PenStyle.SolidLine) Painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(3) Pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush) Pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode # white when dark mode, black when light mode
pen.setColor(self.getMarkColor()) Pen.setColor(self.getMarkColor())
painter.setPen(pen) Painter.setPen(Pen)
mark_size = radius/2 mark_size = radius/2
mark_path = [ mark_path = [
(center_x - mark_size, center_y), (center_x - mark_size, center_y),
(center_x - mark_size/3, center_y + mark_size/2), (center_x - mark_size/3, center_y + mark_size/2),
(center_x + mark_size, 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[0][0]),int(mark_path[0][1]),
int(mark_path[1][0]),int(mark_path[1][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[1][0]),int(mark_path[1][1]),
int(mark_path[2][0]),int(mark_path[2][1]) int(mark_path[2][0]),int(mark_path[2][1])
) )
case self.Status.WARNING: case self.Status.WARNING:
# draw the warning orange circle # draw the warning orange circle
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(2) Pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush) Pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#FF9800")) # orange Pen.setColor(QColor("#FF9800")) # orange
painter.setPen(pen) Painter.setPen(Pen)
painter.drawEllipse( Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
# draw the warning exclamation mark '!' # draw the warning exclamation mark '!'
painter.setPen(Qt.PenStyle.SolidLine) Painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(3) Pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush) Pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode # white when dark mode, black when light mode
pen.setColor(self.getMarkColor()) Pen.setColor(self.getMarkColor())
painter.setPen(pen) Painter.setPen(Pen)
painter.drawLine( Painter.drawLine(
int(center_x), int(center_y - radius/2), int(center_x), int(center_y - radius/2),
int(center_x), int(center_y + radius/6) int(center_x), int(center_y + radius/6)
) )
painter.drawPoint( Painter.drawPoint(
int(center_x), int(center_y + radius/2) int(center_x), int(center_y + radius/2)
) )
case self.Status.FAILURE: case self.Status.FAILURE:
# draw the failure red circle # draw the failure red circle
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(2) Pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush) Pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#DC0000")) # red Pen.setColor(QColor("#DC0000")) # red
painter.setPen(pen) Painter.setPen(Pen)
painter.drawEllipse( Painter.drawEllipse(
int(center_x - radius), int(center_x - radius),
int(center_y - radius), int(center_y - radius),
int(radius*2), int(radius*2),
int(radius*2) int(radius*2)
) )
# draw the failure cross mark '✗' # draw the failure cross mark '✗'
painter.setPen(Qt.PenStyle.SolidLine) Painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen() Pen = Painter.pen()
pen.setWidth(3) Pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush) Pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap) Pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode # white when dark mode, black when light mode
pen.setColor(self.getMarkColor()) Pen.setColor(self.getMarkColor())
painter.setPen(pen) Painter.setPen(Pen)
mark_size = radius/3 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),
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),
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) super().paintEvent(event)
+11 -13
View File
@@ -80,7 +80,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60)) self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit) self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget) self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
self.RelativeTimerWidget = QWidget() self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget) self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0) self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0)
@@ -108,17 +107,16 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox) self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox)
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget) self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False) self.RelativeTimerWidget.setVisible(False)
self.AutoScriptGroupBox = QGroupBox("AutoScript 指令") self.AutoScriptGroupBox = QGroupBox("AutoScript 指令")
self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox) self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox)
self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3) self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3)
self.AutoScriptLayout.setSpacing(3) self.AutoScriptLayout.setSpacing(3)
autoScriptBtnLayout = QHBoxLayout() AutoScriptBtnLayout = QHBoxLayout()
self.AutoScriptEditButton = QPushButton("编辑") self.AutoScriptEditButton = QPushButton("编辑")
self.AutoScriptEditButton.setMinimumHeight(25) self.AutoScriptEditButton.setMinimumHeight(25)
self.AutoScriptEditButton.setFixedWidth(80) self.AutoScriptEditButton.setFixedWidth(80)
autoScriptBtnLayout.addWidget(self.AutoScriptEditButton) AutoScriptBtnLayout.addWidget(self.AutoScriptEditButton)
autoScriptBtnLayout.addStretch() AutoScriptBtnLayout.addStretch()
self.AutoScriptHelpButton = QPushButton("?") self.AutoScriptHelpButton = QPushButton("?")
self.AutoScriptHelpButton.setFixedSize(20, 20) self.AutoScriptHelpButton.setFixedSize(20, 20)
self.AutoScriptHelpButton.setToolTip( self.AutoScriptHelpButton.setToolTip(
@@ -132,12 +130,12 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
"font-weight: bold; color: #555; }" "font-weight: bold; color: #555; }"
"QPushButton:hover { background-color: #E0E0E0; }" "QPushButton:hover { background-color: #E0E0E0; }"
) )
autoScriptBtnLayout.addWidget(self.AutoScriptHelpButton) AutoScriptBtnLayout.addWidget(self.AutoScriptHelpButton)
self.AutoScriptStatusLabel = QLabel("未设置") self.AutoScriptStatusLabel = QLabel("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
self.AutoScriptStatusLabel.setFixedHeight(25) self.AutoScriptStatusLabel.setFixedHeight(25)
autoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel) AutoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel)
self.AutoScriptLayout.addLayout(autoScriptBtnLayout) self.AutoScriptLayout.addLayout(AutoScriptBtnLayout)
self.ALAddTimerTaskLayout.insertWidget( self.ALAddTimerTaskLayout.insertWidget(
self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1, self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1,
self.AutoScriptGroupBox self.AutoScriptGroupBox
@@ -305,18 +303,18 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
@Slot() @Slot()
def onPreviewAutoScript(self): def onPreviewAutoScript(self):
from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog
dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data) Dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data)
if dlg.exec() == QDialog.DialogCode.Accepted: if Dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript() script = Dlg.getScript()
self.__auto_script = script self.__auto_script = script
self.__mock_target_data = dlg.getMockData() self.__mock_target_data = Dlg.getMockData()
if script: if script:
self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
else: else:
self.AutoScriptStatusLabel.setText("未设置") self.AutoScriptStatusLabel.setText("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
dlg.deleteLater() Dlg.deleteLater()
@Slot() @Slot()
def onAutoScriptHelp( def onAutoScriptHelp(
-3
View File
@@ -41,7 +41,6 @@ class ALTimerTaskHistoryDialog(QDialog):
self.setWindowTitle("定时任务执行历史 - AutoLibrary") self.setWindowTitle("定时任务执行历史 - AutoLibrary")
self.setMinimumSize(300, 300) self.setMinimumSize(300, 300)
self.setMaximumSize(500, 400) self.setMaximumSize(500, 400)
MainLayout = QVBoxLayout(self) MainLayout = QVBoxLayout(self)
InfoLayout = QGridLayout() InfoLayout = QGridLayout()
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}") TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
@@ -51,7 +50,6 @@ class ALTimerTaskHistoryDialog(QDialog):
TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;") TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;")
InfoLayout.addWidget(TaskUUIDLabel, 1, 0) InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
InfoLayout.setColumnStretch(0, 1) InfoLayout.setColumnStretch(0, 1)
if self.__task_data.get("repeat", False): if self.__task_data.get("repeat", False):
RepeatLabel = QLabel("可重复性任务") RepeatLabel = QLabel("可重复性任务")
RepeatLabel.setStyleSheet("color: #2294FF; font-size: 12px;") RepeatLabel.setStyleSheet("color: #2294FF; font-size: 12px;")
@@ -68,7 +66,6 @@ class ALTimerTaskHistoryDialog(QDialog):
self.HistoryTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.HistoryTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.loadHistory() self.loadHistory()
MainLayout.addWidget(self.HistoryTableWidget) MainLayout.addWidget(self.HistoryTableWidget)
ButtonLayout = QHBoxLayout() ButtonLayout = QHBoxLayout()
ButtonLayout.addStretch() ButtonLayout.addStretch()
self.CloseButton = QPushButton("关闭") self.CloseButton = QPushButton("关闭")
+44 -44
View File
@@ -173,20 +173,20 @@ class ALTimerTaskItemWidget(QWidget):
pos pos
): ):
menu = QMenu(self) Menu = QMenu(self)
edit_action = QAction("编辑", self) EditAction = QAction("编辑", self)
edit_action.triggered.connect( EditAction.triggered.connect(
lambda: self.editRequested.emit(self.__timer_task) lambda: self.editRequested.emit(self.__timer_task)
) )
menu.addAction(edit_action) Menu.addAction(EditAction)
if self.__timer_task["status"] != ALTimerTaskStatus.RUNNING\ if self.__timer_task["status"] != ALTimerTaskStatus.RUNNING\
and self.__timer_task["status"] != ALTimerTaskStatus.READY: and self.__timer_task["status"] != ALTimerTaskStatus.READY:
delete_action = QAction("删除", self) DeleteAction = QAction("删除", self)
delete_action.triggered.connect( DeleteAction.triggered.connect(
lambda: self.__manage_widget.deleteTask(self.__timer_task) lambda: self.__manage_widget.deleteTask(self.__timer_task)
) )
menu.addAction(delete_action) Menu.addAction(DeleteAction)
menu.exec(self.mapToGlobal(pos)) Menu.exec(self.mapToGlobal(pos))
class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
@@ -209,7 +209,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
super().__init__(parent) super().__init__(parent)
self.__cfg_mgr: ConfigProvider = ConfigManager.instance() self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
self.__timer_tasks = [] self.__timer_tasks = []
self.__check_timer = None self.__CheckTimer = None
self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
self.__sort_order = Qt.SortOrder.AscendingOrder self.__sort_order = Qt.SortOrder.AscendingOrder
@@ -233,9 +233,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self self
): ):
self.__check_timer = QTimer(self) self.__CheckTimer = QTimer(self)
self.__check_timer.timeout.connect(self.checkTasks) self.__CheckTimer.timeout.connect(self.checkTasks)
self.__check_timer.start(500) self.__CheckTimer.start(500)
def initializeTimerTasks( def initializeTimerTasks(
self self
@@ -386,28 +386,28 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.TimerTasksListWidget.clear() self.TimerTasksListWidget.clear()
self.sortTimerTasks(self.__sort_policy, self.__sort_order) self.sortTimerTasks(self.__sort_policy, self.__sort_order)
for timer_task in self.__timer_tasks: for timer_task in self.__timer_tasks:
item = QListWidgetItem() Item = QListWidgetItem()
item.setData(Qt.UserRole, timer_task) Item.setData(Qt.UserRole, timer_task)
widget = ALTimerTaskItemWidget(self, timer_task) Widget = ALTimerTaskItemWidget(self, timer_task)
widget.DeleteButton.clicked.connect( Widget.DeleteButton.clicked.connect(
lambda _, task = timer_task: self.deleteTask(task) lambda _, task = timer_task: self.deleteTask(task)
) )
if timer_task.get("repeat", False) and hasattr(widget, "HistoryButton"): if timer_task.get("repeat", False) and hasattr(Widget, "HistoryButton"):
widget.HistoryButton.clicked.connect( Widget.HistoryButton.clicked.connect(
lambda _, task = timer_task: self.showTaskHistory(task) lambda _, task = timer_task: self.showTaskHistory(task)
) )
widget.editRequested.connect(self.editTask) Widget.editRequested.connect(self.editTask)
item.setSizeHint(widget.size()) Item.setSizeHint(Widget.size())
self.TimerTasksListWidget.addItem(item) self.TimerTasksListWidget.addItem(Item)
self.TimerTasksListWidget.setItemWidget(item, widget) self.TimerTasksListWidget.setItemWidget(Item, Widget)
def addTask( def addTask(
self self
): ):
dialog = ALTimerTaskAddDialog(self) Dialog = ALTimerTaskAddDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted: if Dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = dialog.getTimerTask() timer_task = Dialog.getTimerTask()
self.__timer_tasks.append(timer_task) self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
@@ -416,9 +416,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_task: dict timer_task: dict
): ):
dialog = ALTimerTaskAddDialog(self, timer_task) Dialog = ALTimerTaskAddDialog(self, timer_task)
if dialog.exec() == QDialog.DialogCode.Accepted: if Dialog.exec() == QDialog.DialogCode.Accepted:
updated = dialog.getTimerTask() updated = Dialog.getTimerTask()
for i, task in enumerate(self.__timer_tasks): for i, task in enumerate(self.__timer_tasks):
if task["uuid"] == updated["uuid"]: if task["uuid"] == updated["uuid"]:
self.__timer_tasks[i] = updated self.__timer_tasks[i] = updated
@@ -449,19 +449,19 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
): ):
if timer_task["repeat"]: # when delete a repeat task if timer_task["repeat"]: # when delete a repeat task
msgbox = QMessageBox(self) MsgBox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question) MsgBox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary") MsgBox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons( MsgBox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
) )
msgbox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?") MsgBox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?")
msgbox.setDetailedText( MsgBox.setDetailedText(
"以下可重复性任务将被删除:\n"\ "以下可重复性任务将被删除:\n"\
"\n" "\n"
f"{self.getTimerTaskDetailMessage(timer_task)}" f"{self.getTimerTaskDetailMessage(timer_task)}"
) )
result = msgbox.exec() result = MsgBox.exec()
if result != QMessageBox.StandardButton.Yes: if result != QMessageBox.StandardButton.Yes:
return return
task_uuid = timer_task["uuid"] task_uuid = timer_task["uuid"]
@@ -506,13 +506,13 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
] ]
repeat_tasks_count = len(repeat_tasks) repeat_tasks_count = len(repeat_tasks)
if repeat_tasks_count > 0: if repeat_tasks_count > 0:
msgbox = QMessageBox(self) MsgBox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question) MsgBox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary") MsgBox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons( MsgBox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
) )
msgbox.setText( MsgBox.setText(
f"存在 {repeat_tasks_count} 个可重复性任务,\n" f"存在 {repeat_tasks_count} 个可重复性任务,\n"
"删除可重复性任务将同时删除所有已执行的记录 !\n" "删除可重复性任务将同时删除所有已执行的记录 !\n"
"是否继续 ?" "是否继续 ?"
@@ -520,12 +520,12 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
delete_msgs = [ delete_msgs = [
self.getTimerTaskDetailMessage(x) for x in repeat_tasks self.getTimerTaskDetailMessage(x) for x in repeat_tasks
] ]
msgbox.setDetailedText( MsgBox.setDetailedText(
"以下可重复性任务将被删除:\n"\ "以下可重复性任务将被删除:\n"\
"\n" "\n"
f"{"\n\n".join(delete_msgs)}" f"{"\n\n".join(delete_msgs)}"
) )
result = msgbox.exec() result = MsgBox.exec()
if result != QMessageBox.StandardButton.Yes: if result != QMessageBox.StandardButton.Yes:
return return
# clear all tasks # clear all tasks
@@ -537,8 +537,8 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
task: dict task: dict
): ):
dialog = ALTimerTaskHistoryDialog(self, task) Dialog = ALTimerTaskHistoryDialog(self, task)
if dialog.exec() == QDialog.DialogCode.Accepted: if Dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def checkTasks( def checkTasks(
+15 -15
View File
@@ -51,9 +51,9 @@ class ALUserTreeWidget(QTreeWidget):
self self
): ):
__qtreewidgetitem = QTreeWidgetItem() __QTreeWidgetItem = QTreeWidgetItem()
__qtreewidgetitem.setText(0, u"\u5206\u7ec4/\u7528\u6237"); __QTreeWidgetItem.setText(0, u"\u5206\u7ec4/\u7528\u6237");
self.setHeaderItem(__qtreewidgetitem) self.setHeaderItem(__QTreeWidgetItem)
self.setObjectName(u"UserTreeWidget") self.setObjectName(u"UserTreeWidget")
self.setMinimumSize(QSize(230, 0)) self.setMinimumSize(QSize(230, 0))
self.setMaximumSize(QSize(250, 16777215)) self.setMaximumSize(QSize(250, 16777215))
@@ -81,8 +81,8 @@ class ALUserTreeWidget(QTreeWidget):
self self
): ):
___qtreewidgetitem = self.headerItem() ___QTreeWidgetItem = self.headerItem()
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None)); ___QTreeWidgetItem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
@staticmethod @staticmethod
def isDragPositionValid( def isDragPositionValid(
@@ -109,27 +109,27 @@ class ALUserTreeWidget(QTreeWidget):
super().dragMoveEvent(event) super().dragMoveEvent(event)
source_item = self.currentItem() SourceItem = self.currentItem()
target_item = self.itemAt(event.position().toPoint()) TargetItem = self.itemAt(event.position().toPoint())
if source_item is None: if SourceItem is None:
event.ignore() event.ignore()
return return
if source_item.type() == ALUserTreeItemType.GROUP.value: if SourceItem.type() == ALUserTreeItemType.GROUP.value:
if target_item is not None: if TargetItem is not None:
event.ignore() event.ignore()
return return
elif source_item.type() == ALUserTreeItemType.USER.value: elif SourceItem.type() == ALUserTreeItemType.USER.value:
if target_item is None: if TargetItem is None:
event.ignore() event.ignore()
return return
if target_item.type() != ALUserTreeItemType.GROUP.value: if TargetItem.type() != ALUserTreeItemType.GROUP.value:
event.ignore() event.ignore()
return return
if target_item.checkState(1) == Qt.CheckState.Unchecked: if TargetItem.checkState(1) == Qt.CheckState.Unchecked:
event.ignore() event.ignore()
return return
if not self.isDragPositionValid( if not self.isDragPositionValid(
self.visualItemRect(target_item), self.visualItemRect(TargetItem),
event.position().toPoint() event.position().toPoint()
): ):
event.ignore() event.ignore()
-6
View File
@@ -182,14 +182,11 @@ class ALWebDriverDownloadDialog(QDialog):
self.setMaximumHeight(240) self.setMaximumHeight(240)
self.setMinimumHeight(240) self.setMinimumHeight(240)
self.setWindowTitle("浏览器驱动下载 - AutoLibrary") self.setWindowTitle("浏览器驱动下载 - AutoLibrary")
self.MainLayout = QVBoxLayout(self) self.MainLayout = QVBoxLayout(self)
self.MainLayout.setContentsMargins(5, 5, 5, 5) self.MainLayout.setContentsMargins(5, 5, 5, 5)
self.MainLayout.setSpacing(5) self.MainLayout.setSpacing(5)
self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:") self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:")
self.MainLayout.addWidget(self.BrowserCountLabel) self.MainLayout.addWidget(self.BrowserCountLabel)
self.DriverInfoLayout = QHBoxLayout() self.DriverInfoLayout = QHBoxLayout()
self.DriverInfoLayout.setSpacing(5) self.DriverInfoLayout.setSpacing(5)
self.DriverComboBox = QComboBox() self.DriverComboBox = QComboBox()
@@ -198,7 +195,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.StatusLabel.setFixedSize(32, 32) self.StatusLabel.setFixedSize(32, 32)
self.DriverInfoLayout.addWidget(self.StatusLabel) self.DriverInfoLayout.addWidget(self.StatusLabel)
self.MainLayout.addLayout(self.DriverInfoLayout) self.MainLayout.addLayout(self.DriverInfoLayout)
self.DetailLayout = QVBoxLayout() self.DetailLayout = QVBoxLayout()
self.DetailLayout.setSpacing(5) self.DetailLayout.setSpacing(5)
self.DetailLayout.setContentsMargins(5, 5, 5, 5) self.DetailLayout.setContentsMargins(5, 5, 5, 5)
@@ -211,7 +207,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.PathLabel.setText("路径:未安装") self.PathLabel.setText("路径:未安装")
self.DetailLayout.addWidget(self.PathLabel) self.DetailLayout.addWidget(self.PathLabel)
self.MainLayout.addLayout(self.DetailLayout) self.MainLayout.addLayout(self.DetailLayout)
self.Line = QFrame() self.Line = QFrame()
self.Line.setFrameShape(QFrame.Shape.HLine) self.Line.setFrameShape(QFrame.Shape.HLine)
self.Line.setFrameShadow(QFrame.Shadow.Sunken) self.Line.setFrameShadow(QFrame.Shadow.Sunken)
@@ -237,7 +232,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.ConfirmButton = QPushButton("确认") self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25) self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setEnabled(False) self.ConfirmButton.setEnabled(False)
self.ControlLayout.addWidget(self.RefreshButton) self.ControlLayout.addWidget(self.RefreshButton)
self.ControlLayout.addWidget(self.DownloadButton) self.ControlLayout.addWidget(self.DownloadButton)
self.ControlLayout.addWidget(self.DeleteButton) 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: This software is provided "as is", without any warranty of any kind.
- ALMainWindow: Main window class. You may use, modify, and distribute this file under the terms of the MIT License.
- ALAboutDialog: About dialog class. See the LICENSE file for details.
- 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.
"""
+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"> <property name="maximumSize">
<size> <size>
<width>800</width> <width>800</width>
<height>400</height> <height>600</height>
</size> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -103,53 +103,6 @@
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
</property> </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> </layout>
</item> </item>
<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 This software is provided "as is", without any warranty of any kind.
used across layers to decouple consumers from concrete implementations. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
Key components:
- ConfigProvider: Abstract interface for configuration access.
- ConfigType: Enumeration of configuration file types.
- ConfigKey: Type-safe hierarchical key constants for config lookups.
""" """
+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: This software is provided "as is", without any warranty of any kind.
- ConfigManager: Config manager for managing configuration files. You may use, modify, and distribute this file under the terms of the MIT License.
- LogManager: Log manager for logging. See the LICENSE file for details.
- WebDriverManager: Web driver manager for managing web drivers. """
"""
+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: This software is provided "as is", without any warranty of any kind.
- ConfigManager: Config manager for managing configuration files. 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 break
if not driver_file: if not driver_file:
raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}") 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, "解压完成") progress_callback(100, 100, 0.0, "解压完成")
self.download_path.unlink() self.download_path.unlink()
self._cleanup(driver_file) self._cleanup(driver_file)
+4
View File
@@ -111,6 +111,10 @@ class WebDriverManager:
for driver_info in self.__driver_infos: for driver_info in self.__driver_infos:
driver_path = self._getDriverPath(driver_info) driver_path = self._getDriverPath(driver_info)
if driver_path and driver_path.exists() and driver_path.is_file(): 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_path = driver_path
driver_info.driver_status = WebDriverStatus.INSTALLED 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: This software is provided "as is", without any warranty of any kind.
- WebBrowserDetector: Web browser detector class. You may use, modify, and distribute this file under the terms of the MIT License.
- WebDriverDownloader: Web driver downloader class. See the LICENSE file for details.
- WebDriverManager: Web driver manager class. """
"""
+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: This software is provided "as is", without any warranty of any kind.
- LogManager: Log manager for logging. 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 os
import queue import queue
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.common.exceptions import ( from selenium.common.exceptions import (
TimeoutException, TimeoutException,
WebDriverException, WebDriverException,
@@ -37,11 +38,11 @@ class AutoLib(MsgBase):
output_queue: queue.Queue, output_queue: queue.Queue,
run_config: dict, run_config: dict,
) -> None: ) -> None:
super().__init__(input_queue, output_queue)
super().__init__(input_queue, output_queue)
self.__run_config: dict = run_config self.__run_config: dict = run_config
self.__user_config: dict | None = None self.__user_config: dict | None = None
self.__driver = None self.__driver: WebDriver | None = None
self.__driver_type: str = "" self.__driver_type: str = ""
self.__driver_path: str = "" self.__driver_path: str = ""
self.__login_page: LoginPage = None self.__login_page: LoginPage = None
@@ -58,7 +59,7 @@ class AutoLib(MsgBase):
else: else:
if not self.__initDriverUrl(): if not self.__initDriverUrl():
self.close() self.close()
raise Exception("浏览器驱动URL初始化失败 !") raise Exception("浏览器驱动 URL 初始化失败 !")
self.__initPagesServices() self.__initPagesServices()
self.__initPagesFlows() self.__initPagesFlows()
@@ -67,9 +68,10 @@ class AutoLib(MsgBase):
) -> bool: ) -> bool:
self._showTrace("正在初始化浏览器驱动......", no_log=True) self._showTrace("正在初始化浏览器驱动......", no_log=True)
web_driver_config: dict = self.__run_config.get("web_driver", None) driver_config: dict = self.__run_config.get("web_driver", None)
self.__driver_type = web_driver_config.get("driver_type", "none") self.__driver_type = driver_config.get("driver_type", "none")
match self.__driver_type.lower(): self.__driver_type = self.__driver_type.lower()
match self.__driver_type:
case "edge": case "edge":
driver_options = webdriver.EdgeOptions() driver_options = webdriver.EdgeOptions()
case "chrome": case "chrome":
@@ -82,10 +84,10 @@ class AutoLib(MsgBase):
self.TraceLevel.WARNING, self.TraceLevel.WARNING,
) )
return False return False
if not web_driver_config: if not driver_config:
self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR) self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
return False return False
if web_driver_config.get("headless", False): if driver_config.get("headless", False):
driver_options.add_argument("--headless") driver_options.add_argument("--headless")
driver_options.add_argument("--disable-gpu") driver_options.add_argument("--disable-gpu")
driver_options.add_argument("--no-sandbox") driver_options.add_argument("--no-sandbox")
@@ -110,11 +112,11 @@ class AutoLib(MsgBase):
"AppleWebKit/537.36 (KHTML, like Gecko) "\ "AppleWebKit/537.36 (KHTML, like Gecko) "\
"Chrome/120.0.0.0 "\ "Chrome/120.0.0.0 "\
"Safari/537.36" "Safari/537.36"
if self.__driver_type.lower() == "edge": if self.__driver_type == "edge":
user_agent += " Edg/120.0.0.0" user_agent += " Edg/120.0.0.0"
# set options for firefox # 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("dom.webdriver.enabled", False)
driver_options.set_preference("useAutomationExtension", False) driver_options.set_preference("useAutomationExtension", False)
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\ 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}") driver_options.add_argument(f"user-agent={user_agent}")
# init browser driver # 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: if not self.__driver_path:
self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING) self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING)
return False return False
try: try:
self.__driver_path = os.path.abspath(self.__driver_path) self.__driver_path = os.path.abspath(self.__driver_path)
service = None service = None
match self.__driver_type.lower(): match self.__driver_type:
case "edge": case "edge":
service = EdgeService(executable_path=self.__driver_path) service = EdgeService(executable_path=self.__driver_path)
self.__driver = webdriver.Edge(service=service, options=driver_options) self.__driver = webdriver.Edge(service=service, options=driver_options)
@@ -149,9 +151,6 @@ class AutoLib(MsgBase):
except WebDriverException as e: except WebDriverException as e:
self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR) self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
return False return False
except Exception as e:
self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
return False
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}") self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
return True return True
@@ -164,7 +163,7 @@ class AutoLib(MsgBase):
self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR) self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR)
return False return False
url: str = lib_config.get("host_url") + lib_config.get("login_url") 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) self.__driver.set_page_load_timeout(5)
try: try:
self.__driver.get(url) self.__driver.get(url)
@@ -236,6 +235,7 @@ class AutoLib(MsgBase):
# result : -1 - terminate, 0 - success, 1 - failed, 2 - passed # result : -1 - terminate, 0 - success, 1 - failed, 2 - passed
result: int = 2 result: int = 2
# login # login
auto_captcha: bool = login_config.get("auto_captcha", True) auto_captcha: bool = login_config.get("auto_captcha", True)
if not self.__login_page.login( if not self.__login_page.login(
@@ -252,10 +252,11 @@ class AutoLib(MsgBase):
"auto_checkin": run_mode_raw & 0x2, "auto_checkin": run_mode_raw & 0x2,
"auto_renewal": run_mode_raw & 0x4, "auto_renewal": run_mode_raw & 0x4,
} }
# reserve # reserve
if run_mode["auto_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( ctx = ReserveContext(
username=username, username=username,
date=reserve_info["date"], date=reserve_info["date"],
@@ -276,10 +277,10 @@ class AutoLib(MsgBase):
else: else:
result = 1 result = 1
else: else:
result = 1 self._showTrace(f"用户 {username} 无法预约, 已跳过")
result = 2
else: else:
self._showTrace(f"用户 {username} 无法预约, 已跳过") result = 1
result = 2
# checkin # checkin
last_result: int = result last_result: int = result
@@ -320,8 +321,11 @@ class AutoLib(MsgBase):
# logout # logout
if not self.__shell.logout(): if not self.__shell.logout():
self._showTrace(f"用户 {username} 退出登录失败, 尝试直接重载页面")
if not self.__initDriverUrl(): if not self.__initDriverUrl():
self._showTrace(f"用户 {username} 重载页面失败, 无法继续操作, 该任务已终止 !")
return -1 return -1
self._showTrace(f"用户 {username} 已退出登录")
return result return result
def run( 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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from typing import Callable, Optional import queue
from typing import Callable
from selenium.common.exceptions import ( from selenium.common.exceptions import (
ElementNotInteractableException, ElementNotInteractableException,
@@ -19,8 +20,10 @@ from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from base.MsgBase import MsgBase
class LoginPage:
class LoginPage(MsgBase):
USERNAME_INPUT = (By.NAME, "username") USERNAME_INPUT = (By.NAME, "username")
PASSWORD_INPUT = (By.NAME, "password") PASSWORD_INPUT = (By.NAME, "password")
@@ -36,22 +39,13 @@ class LoginPage:
def __init__( def __init__(
self, self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver: WebDriver, driver: WebDriver,
tracer: Optional[Callable[..., None]] = None,
) -> None: ) -> None:
super().__init__(input_queue, output_queue)
self._driver: WebDriver = driver 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( def navigate(
self, self,
@@ -85,9 +79,7 @@ class LoginPage:
EC.presence_of_element_located(self.CAPTCHA_IMG) EC.presence_of_element_located(self.CAPTCHA_IMG)
) )
return True return True
except (NoSuchElementException, TimeoutException): except TimeoutException:
return False
except Exception:
return False return False
def fillCredentials( def fillCredentials(
@@ -104,17 +96,21 @@ class LoginPage:
el.clear() el.clear()
el.send_keys(password) el.send_keys(password)
return True return True
except (NoSuchElementException, TimeoutException): except (NoSuchElementException, ElementNotInteractableException):
return False
except Exception:
return False return False
def getCaptchaImageSrc( def getCaptchaImageSrc(
self, self,
) -> str: ) -> str | None:
captcha_el = self._driver.find_element(*self.CAPTCHA_IMG) # return 'None' if captcha image element is not found.
return captcha_el.get_attribute("src") # 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( def refreshCaptcha(
self, self,
@@ -123,10 +119,7 @@ class LoginPage:
try: try:
self._driver.find_element(*self.CAPTCHA_IMG).click() self._driver.find_element(*self.CAPTCHA_IMG).click()
return True return True
except (NoSuchElementException, TimeoutException, except (NoSuchElementException, ElementNotInteractableException):
ElementNotInteractableException):
return False
except Exception:
return False return False
def fillCaptcha( def fillCaptcha(
@@ -139,9 +132,7 @@ class LoginPage:
el.clear() el.clear()
el.send_keys(captcha_text) el.send_keys(captcha_text)
return True return True
except (NoSuchElementException, TimeoutException): except (NoSuchElementException, ElementNotInteractableException):
return False
except Exception:
return False return False
def clickLogin( def clickLogin(
@@ -151,10 +142,7 @@ class LoginPage:
try: try:
self._driver.find_element(*self.LOGIN_BUTTON).click() self._driver.find_element(*self.LOGIN_BUTTON).click()
return True return True
except (NoSuchElementException, TimeoutException, except (NoSuchElementException, ElementNotInteractableException):
ElementNotInteractableException):
return False
except Exception:
return False return False
def waitLoginSuccess( def waitLoginSuccess(
@@ -172,9 +160,7 @@ class LoginPage:
EC.presence_of_element_located(self.SUCCESS_INDICATOR_CONTENT) EC.presence_of_element_located(self.SUCCESS_INDICATOR_CONTENT)
) )
return True return True
except (NoSuchElementException, TimeoutException): except TimeoutException:
return False
except Exception:
return False return False
def stopPageLoad( def stopPageLoad(
@@ -193,7 +179,7 @@ class LoginPage:
) -> bool: ) -> bool:
for attempt in range(max_attempts): for attempt in range(max_attempts):
self._trace( self._showTrace(
f"用户 {username}{attempt + 1} 次尝试登录......", f"用户 {username}{attempt + 1} 次尝试登录......",
no_log=True, no_log=True,
) )
@@ -204,16 +190,16 @@ class LoginPage:
continue continue
if not self.fillCaptcha(captcha_text): if not self.fillCaptcha(captcha_text):
continue continue
self._trace("尝试登录...", no_log=True) self._showTrace("尝试登录...", no_log=True)
if not self.clickLogin(): if not self.clickLogin():
continue continue
if self.waitLoginSuccess(): if self.waitLoginSuccess():
self._trace(f"用户 {username}{attempt + 1} 次登录成功 !") self._showTrace(f"用户 {username}{attempt + 1} 次登录成功 !")
return True return True
else: else:
self._trace( self._showTrace(
"登录页面加载失败 ! : " "登录页面加载失败 ! : "
"用户账号或者密码错误/验证码错误, 具体以页面提示为准", "用户账号或者密码错误/验证码错误, 具体以页面提示为准",
level=40, level=self.TraceLevel.ERROR,
) )
return False 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.support import expected_conditions as EC
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
from selenium.common.exceptions import ( from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException, NoSuchElementException,
TimeoutException, TimeoutException,
WebDriverException,
) )
from pages.ReserveView import ReserveView from pages.ReserveView import ReserveView
@@ -38,6 +40,15 @@ class MainShell:
self._driver = driver self._driver = driver
def _clickTab(
self,
locator: tuple,
) -> None:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(locator)
).click()
def gotoReserveView( def gotoReserveView(
self, self,
) -> ReserveView: ) -> ReserveView:
@@ -65,9 +76,7 @@ class MainShell:
try: try:
self._driver.find_element(*self.TAB_LOGOUT).click() self._driver.find_element(*self.TAB_LOGOUT).click()
return True return True
except NoSuchElementException: except (NoSuchElementException, ElementNotInteractableException):
return False
except Exception:
return False return False
def waitCheckinButton( def waitCheckinButton(
@@ -81,8 +90,6 @@ class MainShell:
return True return True
except TimeoutException: except TimeoutException:
return False return False
except Exception:
return False
def waitExtendButton( def waitExtendButton(
self, self,
@@ -95,40 +102,50 @@ class MainShell:
return True return True
except TimeoutException: except TimeoutException:
return False return False
except Exception:
return False
def isCheckinButtonDisabled( def isCheckinButtonDisabled(
self, self,
) -> bool: ) -> bool:
btn = self._driver.find_element(*self.BTN_CHECKIN) try:
return "disabled" in btn.get_attribute("class") btn = self._driver.find_element(*self.BTN_CHECKIN)
return "disabled" in btn.get_attribute("class")
except NoSuchElementException:
return True
def isExtendButtonDisabled( def isExtendButtonDisabled(
self, self,
) -> bool: ) -> bool:
btn = self._driver.find_element(*self.BTN_EXTEND) try:
return "disabled" in btn.get_attribute("class") btn = self._driver.find_element(*self.BTN_EXTEND)
return "disabled" in btn.get_attribute("class")
except NoSuchElementException:
return True
def clickCheckinButton( def clickCheckinButton(
self, self,
) -> None: ) -> None:
btn = WebDriverWait(self._driver, 2).until( try:
EC.element_to_be_clickable(self.BTN_CHECKIN) btn = WebDriverWait(self._driver, 2).until(
) EC.element_to_be_clickable(self.BTN_CHECKIN)
btn.click() )
btn.click()
except (TimeoutException, ElementNotInteractableException):
return
def clickExtendButton( def clickExtendButton(
self, self,
) -> None: ) -> None:
btn = WebDriverWait(self._driver, 2).until( try:
EC.element_to_be_clickable(self.BTN_EXTEND) btn = WebDriverWait(self._driver, 2).until(
) EC.element_to_be_clickable(self.BTN_EXTEND)
btn.click() )
btn.click()
except (TimeoutException, ElementNotInteractableException):
return
def enableCheckinButtonByJS( def enableCheckinButtonByJS(
self, self,
@@ -154,13 +171,7 @@ class MainShell:
self, self,
) -> None: ) -> None:
self._driver.refresh() try:
self._driver.refresh()
def _clickTab( except (TimeoutException, WebDriverException):
self, return
locator: tuple,
) -> None:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(locator)
).click()
-6
View File
@@ -44,8 +44,6 @@ class RecordsView:
return self._driver.find_elements(*self.RECORDS_LIST) return self._driver.find_elements(*self.RECORDS_LIST)
except TimeoutException: except TimeoutException:
return None return None
except Exception:
return None
def getRecordTimeElement( def getRecordTimeElement(
self, self,
@@ -71,8 +69,6 @@ class RecordsView:
) )
except TimeoutException: except TimeoutException:
return False return False
except Exception:
return False
try: try:
more_btn = self._driver.find_element(*self.MORE_BTN) more_btn = self._driver.find_element(*self.MORE_BTN)
if more_btn.is_displayed() and more_btn.is_enabled(): if more_btn.is_displayed() and more_btn.is_enabled():
@@ -82,8 +78,6 @@ class RecordsView:
return False return False
except (NoSuchElementException, StaleElementReferenceException): except (NoSuchElementException, StaleElementReferenceException):
return False return False
except Exception:
return False
def getRecordText( def getRecordText(
self, self,
+48 -55
View File
@@ -53,6 +53,50 @@ class ReserveView:
self._driver = driver 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( def selectDate(
self, self,
date_str: str, date_str: str,
@@ -109,21 +153,15 @@ class ReserveView:
).click() ).click()
except (TimeoutException, ElementNotInteractableException): except (TimeoutException, ElementNotInteractableException):
return None return None
except Exception:
return None
try: try:
WebDriverWait(self._driver, 2).until( WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable((By.ID, self.ROOM_BTN_FMT.format(room=room))) EC.element_to_be_clickable((By.ID, self.ROOM_BTN_FMT.format(room=room)))
).click() ).click()
except (TimeoutException, ElementNotInteractableException): except (TimeoutException, ElementNotInteractableException):
return None return None
except Exception:
return None
try: try:
return SeatMapDialog(self._driver) return SeatMapDialog(self._driver)
except (TimeoutException): except TimeoutException:
return None
except Exception:
return None return None
def submitReserve( def submitReserve(
@@ -137,57 +175,12 @@ class ReserveView:
return True return True
except (TimeoutException, ElementNotInteractableException): except (TimeoutException, ElementNotInteractableException):
return False return False
except Exception:
return False
def refresh( def refresh(
self, self,
) -> None: ) -> 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: try:
WebDriverWait(self._driver, 2).until( self._driver.refresh()
EC.element_to_be_clickable(trigger) except TimeoutException:
).click() return
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(option)
).click()
return True
except (TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False
+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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from pages.AutoLib import AutoLib from .AutoLib import AutoLib
from pages.LoginPage import LoginPage from .LoginPage import LoginPage
from pages.MainShell import MainShell from .MainShell import MainShell
from pages.ReserveView import ReserveView from .ReserveView import ReserveView
from pages.RecordsView import RecordsView from .RecordsView import RecordsView
from pages.components.SeatMapDialog import SeatMapDialog from .components.SeatMapDialog import SeatMapDialog
from pages.components.TimeSelectDialog import TimeSelectDialog from .components.TimeSelectDialog import TimeSelectDialog
from pages.components.ReserveResultDialog import ReserveResultDialog from .components.ReserveResultDialog import ReserveResultDialog
from pages.components.CheckinResultDialog import CheckinResultDialog from .components.CheckinResultDialog import CheckinResultDialog
from pages.components.RenewDialog import RenewDialog from .components.RenewDialog import RenewDialog
__all__ = [
"AutoLib",
"LoginPage",
"MainShell",
"ReserveView",
"RecordsView",
"SeatMapDialog",
"TimeSelectDialog",
"ReserveResultDialog",
"CheckinResultDialog",
"RenewDialog",
]
+3 -8
View File
@@ -45,9 +45,8 @@ class CheckinResultDialog(Dialog):
self._waitPresence(self.RESULT_MSG) self._waitPresence(self.RESULT_MSG)
el = self._find(*self.RESULT_MSG) el = self._find(*self.RESULT_MSG)
return el.text return el.text
except (TimeoutException, NoSuchElementException, StaleElementReferenceException): except (TimeoutException, NoSuchElementException,
return "" StaleElementReferenceException):
except Exception:
return "" return ""
def getDetails( def getDetails(
@@ -59,8 +58,6 @@ class CheckinResultDialog(Dialog):
return [el.text for el in elements if el.text.strip()] return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException): except (NoSuchElementException, StaleElementReferenceException):
return [] return []
except Exception:
return []
def clickOk( def clickOk(
self, self,
@@ -69,7 +66,5 @@ class CheckinResultDialog(Dialog):
try: try:
self._waitClickable(self.OK_BTN).click() self._waitClickable(self.OK_BTN).click()
return True return True
except (NoSuchElementException, TimeoutException, ElementNotInteractableException): except (TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False return False
+16 -13
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
from selenium.common.exceptions import ( from selenium.common.exceptions import (
ElementNotInteractableException, ElementNotInteractableException,
NoSuchElementException, NoSuchElementException,
StaleElementReferenceException,
TimeoutException, TimeoutException,
) )
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
@@ -50,9 +51,7 @@ class RenewDialog(Dialog):
self._waitVisible(self.ROOT) self._waitVisible(self.ROOT)
self._waitPresence(self.MESSAGE_HEAD) self._waitPresence(self.MESSAGE_HEAD)
self._waitPresence(self.RESULT_MSG) self._waitPresence(self.RESULT_MSG)
except (NoSuchElementException, TimeoutException): except TimeoutException:
return False
except Exception:
return False return False
head_msg = self._find(*self.MESSAGE_HEAD).text.strip() head_msg = self._find(*self.MESSAGE_HEAD).text.strip()
if "警告" in head_msg: if "警告" in head_msg:
@@ -60,9 +59,7 @@ class RenewDialog(Dialog):
try: try:
self._waitAllPresence(self.TIME_OPTS) self._waitAllPresence(self.TIME_OPTS)
self._waitPresence(self.OK_BTN) self._waitPresence(self.OK_BTN)
except (NoSuchElementException, TimeoutException): except TimeoutException:
return False
except Exception:
return False return False
return True return True
@@ -70,13 +67,19 @@ class RenewDialog(Dialog):
self, self,
) -> str: ) -> str:
return self._find(*self.MESSAGE_HEAD).text.strip() try:
return self._find(*self.MESSAGE_HEAD).text.strip()
except (NoSuchElementException, StaleElementReferenceException):
return ""
def getResultMessage( def getResultMessage(
self, self,
) -> str: ) -> str:
return self._find(*self.RESULT_MSG).text.strip() try:
return self._find(*self.RESULT_MSG).text.strip()
except (NoSuchElementException, StaleElementReferenceException):
return ""
def getTimeOptions( def getTimeOptions(
self, self,
@@ -101,7 +104,10 @@ class RenewDialog(Dialog):
prefer_earlier, prefer_earlier,
) )
if result.selected_index >= 0: 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 return result
def getOkButton( def getOkButton(
@@ -117,8 +123,5 @@ class RenewDialog(Dialog):
try: try:
self._find(*self.OK_BTN).click() self._find(*self.OK_BTN).click()
return True return True
except (NoSuchElementException, TimeoutException, except (NoSuchElementException, ElementNotInteractableException):
ElementNotInteractableException):
return False
except Exception:
return False return False
@@ -45,8 +45,6 @@ class ReserveResultDialog(Dialog):
return self._find(*self._titleLocator()).text return self._find(*self._titleLocator()).text
except (NoSuchElementException, StaleElementReferenceException): except (NoSuchElementException, StaleElementReferenceException):
return "" return ""
except Exception:
return ""
def isSuccess( def isSuccess(
self, self,
@@ -77,5 +75,3 @@ class ReserveResultDialog(Dialog):
return [el.text for el in elements if el.text.strip()] return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException): except (NoSuchElementException, StaleElementReferenceException):
return [] return []
except Exception:
return []
+1 -7
View File
@@ -43,9 +43,7 @@ class SeatMapDialog(Dialog):
try: try:
self._waitAllPresence(self.SEAT_ITEMS) self._waitAllPresence(self.SEAT_ITEMS)
except (NoSuchElementException, TimeoutException): except TimeoutException:
return None
except Exception:
return None return None
try: try:
seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}") seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}")
@@ -58,8 +56,6 @@ class SeatMapDialog(Dialog):
except (NoSuchElementException, ValueError, TimeoutException, except (NoSuchElementException, ValueError, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException): ElementNotInteractableException, StaleElementReferenceException):
pass pass
except Exception:
pass
try: try:
all_seats = self._findAll(*self.SEAT_ITEMS) all_seats = self._findAll(*self.SEAT_ITEMS)
seat_id_upper = seat_id.lstrip('0').upper() seat_id_upper = seat_id.lstrip('0').upper()
@@ -76,5 +72,3 @@ class SeatMapDialog(Dialog):
except (NoSuchElementException, TimeoutException, except (NoSuchElementException, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException): ElementNotInteractableException, StaleElementReferenceException):
return None return None
except Exception:
return None
+7 -5
View File
@@ -13,7 +13,8 @@ import logging
from typing import TYPE_CHECKING, Callable, Optional from typing import TYPE_CHECKING, Callable, Optional
from selenium.common.exceptions import ( from selenium.common.exceptions import (
NoSuchElementException, ElementNotInteractableException,
StaleElementReferenceException,
TimeoutException, TimeoutException,
) )
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
@@ -106,9 +107,7 @@ class TimeSelectDialog(Dialog):
self._waitAllPresence( self._waitAllPresence(
(By.CSS_SELECTOR, f"#{time_id} ul li a") (By.CSS_SELECTOR, f"#{time_id} ul li a")
) )
except (NoSuchElementException, TimeoutException): except TimeoutException:
return []
except Exception:
return [] return []
return self._findAll( return self._findAll(
By.CSS_SELECTOR, By.CSS_SELECTOR,
@@ -133,7 +132,10 @@ class TimeSelectDialog(Dialog):
prefer_earlier, prefer_earlier,
) )
if result.selected_index >= 0: 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 return result
def selectTimeRange( 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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from pages.components.SeatMapDialog import SeatMapDialog from .SeatMapDialog import SeatMapDialog
from pages.components.TimeSelectDialog import TimeSelectDialog from .TimeSelectDialog import TimeSelectDialog
from pages.components.ReserveResultDialog import ReserveResultDialog from .ReserveResultDialog import ReserveResultDialog
from pages.components.CheckinResultDialog import CheckinResultDialog from .CheckinResultDialog import CheckinResultDialog
from pages.components.RenewDialog import RenewDialog from .RenewDialog import RenewDialog
__all__ = [
"SeatMapDialog",
"TimeSelectDialog",
"ReserveResultDialog",
"CheckinResultDialog",
"RenewDialog",
]
+19 -5
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
import queue import queue
from selenium.common.exceptions import ( from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException, NoSuchElementException,
TimeoutException, TimeoutException,
) )
@@ -34,7 +35,7 @@ class CheckinFlow(MsgBase):
self._driver: WebDriver = driver self._driver: WebDriver = driver
self._shell: MainShell = shell self._shell: MainShell = shell
def execute( def _ensureCheckinButton(
self, self,
username: str, username: str,
) -> bool: ) -> bool:
@@ -48,7 +49,13 @@ class CheckinFlow(MsgBase):
self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR) self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
return False return False
self._showTrace("签到按钮已启用") self._showTrace("签到按钮已启用")
self._shell.clickCheckinButton() return True
def _processCheckinDialog(
self,
username: str,
) -> bool:
try: try:
with CheckinResultDialog(self._driver) as dialog: with CheckinResultDialog(self._driver) as dialog:
result_msg = dialog.getResultMessage() result_msg = dialog.getResultMessage()
@@ -83,9 +90,16 @@ class CheckinFlow(MsgBase):
dialog.clickOk() dialog.clickOk()
self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR) self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR)
return False return False
except (NoSuchElementException, TimeoutException): except (TimeoutException, NoSuchElementException, ElementNotInteractableException):
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
return False return False
except Exception:
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) def execute(
self,
username: str,
) -> bool:
if not self._ensureCheckinButton(username):
return False 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._driver: WebDriver = driver
self._shell: MainShell = shell self._shell: MainShell = shell
def execute( def _validateRenewTime(
self, self,
username: str, end_time: str,
record: dict, target_renew_mins: int,
renew_info: dict,
) -> bool: ) -> bool:
max_diff = renew_info.get("max_diff", 30) if target_renew_mins > self.LIBRARY_CLOSE_MINS:
prefer_earlier = renew_info.get("prefer_early", True) 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"] end_time = record["time"]["end"]
target_renew_mins = timeStrToMins(end_time) + renew_info.get("expect_duration", 2) * 60 target_renew_mins = timeStrToMins(end_time) + renew_info.get("expect_duration", 2) * 60
if not self._validateRenewTime(end_time, target_renew_mins): 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(): if not self._shell.waitExtendButton():
self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR) self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR)
return False return False
@@ -61,7 +87,17 @@ class RenewFlow(MsgBase):
f"请连接图书馆网络后重试" f"请连接图书馆网络后重试"
) )
return False 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: try:
with RenewDialog(self._driver) as dialog: with RenewDialog(self._driver) as dialog:
if not dialog.waitUntilReady(): if not dialog.waitUntilReady():
@@ -106,37 +142,26 @@ class RenewFlow(MsgBase):
self._showTrace(f"当前可供续约的时间有: {result.free_times}") self._showTrace(f"当前可供续约的时间有: {result.free_times}")
self._shell.refresh() self._shell.refresh()
return False return False
except (NoSuchElementException, TimeoutException) as e: except (NoSuchElementException, TimeoutException, ElementNotInteractableException) 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:
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR) self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
self._shell.refresh() self._shell.refresh()
return False return False
def _validateRenewTime( def execute(
self, self,
end_time: str, username: str,
target_renew_mins: int, record: dict,
renew_info: dict,
) -> bool: ) -> bool:
if target_renew_mins > self.LIBRARY_CLOSE_MINS: max_diff = renew_info.get("max_diff", 30)
actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time) prefer_earlier = renew_info.get("prefer_early", True)
if actual_renew_duration <= 0: target_renew_mins = self._computeRenewTarget(record, renew_info)
self._showTrace( if target_renew_mins is None:
f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR return False
) if not self._ensureExtendButton(username):
return False return False
self._showTrace( self._shell.clickExtendButton()
f"续约时间已调整至闭馆时间 " return self._processRenewDialog(
f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)}," username, record, target_renew_mins, max_diff, prefer_earlier,
f"实际续约时长为 " )
f"{actual_renew_duration // 60} 小时 "
f"{actual_renew_duration % 60} 分钟"
)
return True
+103 -41
View File
@@ -12,7 +12,6 @@ from dataclasses import dataclass
from selenium.common.exceptions import ( from selenium.common.exceptions import (
ElementNotInteractableException, ElementNotInteractableException,
NoSuchElementException,
TimeoutException, TimeoutException,
) )
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
@@ -59,43 +58,105 @@ class ReserveFlow(MsgBase):
self._driver: WebDriver = driver self._driver: WebDriver = driver
self._shell: MainShell = shell self._shell: MainShell = shell
def execute( def _loadReserveView(
self, 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, ctx: ReserveContext,
) -> bool: ) -> 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): if not view.selectDate(ctx.date):
self._showTrace(f"选择日期失败 ! : {ctx.date} 不可用", self.TraceLevel.ERROR) self._showTrace(f"选择日期失败 ! : {ctx.date} 不可用", self.TraceLevel.ERROR)
return False return False
self._showTrace(f"日期 {ctx.date} 选择成功 !") self._showTrace(f"日期 {ctx.date} 选择成功 !")
return True
def _selectPlace(
self,
view: ReserveView,
) -> bool:
if not view.selectPlace("1"): if not view.selectPlace("1"):
self._showTrace("选择预约场所失败 ! : 图书馆 不可用", self.TraceLevel.ERROR) self._showTrace("选择预约场所失败 ! : 图书馆 不可用", self.TraceLevel.ERROR)
return False return False
self._showTrace("预约场所 图书馆 选择成功 !") self._showTrace("预约场所 图书馆 选择成功 !")
return True
def _selectFloor(
self,
view: ReserveView,
ctx: ReserveContext,
) -> bool:
if not view.selectFloor(ctx.floor): if not view.selectFloor(ctx.floor):
display_floor = ReserveView.FLOOR_MAP.get(ctx.floor, ctx.floor) display_floor = ReserveView.FLOOR_MAP.get(ctx.floor, ctx.floor)
self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR) self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR)
return False return False
self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !") self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !")
return True
def _selectRoom(
self,
view: ReserveView,
ctx: ReserveContext,
):
seat_map = view.selectRoom(ctx.room) seat_map = view.selectRoom(ctx.room)
if seat_map is None: if seat_map is None:
display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room) display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room)
self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
return False return None
self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !") 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) seat_status = seat_map.selectSeat(ctx.seat_id)
if seat_status is None: if seat_status is None:
self._showTrace( self._showTrace(
@@ -115,33 +176,34 @@ class ReserveFlow(MsgBase):
try: try:
view.submitReserve() view.submitReserve()
submit_reserve = True submit_reserve = True
with ReserveResultDialog(self._driver) as result: reserve_success = self._processReserveResult()
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)
except (TimeoutException, ElementNotInteractableException): except (TimeoutException, ElementNotInteractableException):
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
except Exception: return submit_reserve, reserve_success
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
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: if not submit_reserve and have_hover_on_page:
view.refresh() view.refresh()
if reserve_success: 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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from pages.flows.ReserveFlow import ReserveFlow from .ReserveFlow import ReserveFlow
from pages.flows.CheckinFlow import CheckinFlow from .CheckinFlow import CheckinFlow
from pages.flows.RenewFlow import RenewFlow from .RenewFlow import RenewFlow
__all__ = [
"ReserveFlow",
"CheckinFlow",
"RenewFlow",
]
+1 -7
View File
@@ -9,11 +9,5 @@ See the LICENSE file for details.
""" """
from pages.strategies.TimeSelectMaker import ( from pages.strategies.TimeSelectMaker import (
minsToTimeStr, minsToTimeStr,
timeStrToMins timeStrToMins,
) )
__all__ = [
"minsToTimeStr",
"timeStrToMins",
]
+11 -26
View File
@@ -11,11 +11,6 @@ import base64
import queue import queue
import ddddocr import ddddocr
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException,
)
from base.MsgBase import MsgBase from base.MsgBase import MsgBase
from pages.LoginPage import LoginPage from pages.LoginPage import LoginPage
@@ -38,6 +33,9 @@ class CaptchaSolver(MsgBase):
try: try:
img_src = login_page.getCaptchaImageSrc() img_src = login_page.getCaptchaImageSrc()
if img_src is None:
self._showTrace("验证码图片元素定位时发生错误 !", self.TraceLevel.ERROR)
return ""
base64_str = img_src.split(',', 1)[1] base64_str = img_src.split(',', 1)[1]
captcha_img = base64.b64decode(base64_str) captcha_img = base64.b64decode(base64_str)
captcha_text = self._ocr.classification(captcha_img) captcha_text = self._ocr.classification(captcha_img)
@@ -45,15 +43,9 @@ class CaptchaSolver(MsgBase):
self._showTrace(f"识别到验证码为 : '{captcha_text}'", 20, no_log=True) self._showTrace(f"识别到验证码为 : '{captcha_text}'", 20, no_log=True)
if len(captcha_text) != 4: if len(captcha_text) != 4:
self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
raise Exception("识别到的验证码长度不等于 4 个字符 !") return ""
return captcha_text return captcha_text
except (NoSuchElementException, TimeoutException) as e: except ValueError 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:
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return "" return ""
@@ -61,20 +53,13 @@ class CaptchaSolver(MsgBase):
self, self,
) -> str: ) -> str:
try: self._showMsg("请输入验证码:")
self._showMsg("请输入验证码:") captcha_text = self._waitMsg(timeout=15)
captcha_text = self._waitMsg(timeout=15) self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True)
self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True) if len(captcha_text) != 4:
if len(captcha_text) != 4: self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
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)
return "" return ""
return captcha_text
def solveCaptcha( def solveCaptcha(
self, self,
+9 -5
View File
@@ -112,20 +112,21 @@ class RecordChecker(MsgBase):
try: try:
time_element = records_view.getRecordTimeElement(reservation) time_element = records_view.getRecordTimeElement(reservation)
info_elements = records_view.getRecordInfoElements(reservation) info_elements = records_view.getRecordInfoElements(reservation)
except (NoSuchElementException, TimeoutException, StaleElementReferenceException): except (NoSuchElementException, StaleElementReferenceException):
return { return {
"date": "", "date": "",
"time": {"begin": "", "end": ""}, "time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""}, "info": {"location": "", "status": ""},
} }
except Exception: try:
time_data = self._decodeReserveTime(time_element)
info_data = self._decodeReserveInfo(info_elements)
except StaleElementReferenceException:
return { return {
"date": "", "date": "",
"time": {"begin": "", "end": ""}, "time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""}, "info": {"location": "", "status": ""},
} }
time_data = self._decodeReserveTime(time_element)
info_data = self._decodeReserveInfo(info_elements)
return { return {
"date": time_data["date"], "date": time_data["date"],
"time": time_data["time"], "time": time_data["time"],
@@ -152,7 +153,10 @@ class RecordChecker(MsgBase):
records_view = shell.gotoRecordsView() records_view = shell.gotoRecordsView()
for _ in range(max_check_times): for _ in range(max_check_times):
reservations = records_view.loadRecords() try:
reservations = records_view.loadRecords()
except TimeoutException:
reservations = None
if reservations is None: if reservations is None:
return None return None
for reservation in reservations[checked_count:]: for reservation in reservations[checked_count:]:
+11
View File
@@ -88,11 +88,22 @@ class ReserveChecker(MsgBase):
) -> bool: ) -> bool:
cur_time = time.strftime("%H:%M", time.localtime()) 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: if reserve_info.get("begin_time") is None:
reserve_info["begin_time"] = {} reserve_info["begin_time"] = {}
if "time" not in reserve_info["begin_time"]: if "time" not in reserve_info["begin_time"]:
reserve_info["begin_time"]["time"] = cur_time reserve_info["begin_time"]["time"] = cur_time
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {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"]: if "max_diff" not in reserve_info["begin_time"]:
reserve_info["begin_time"]["max_diff"] = 30 reserve_info["begin_time"]["max_diff"] = 30
self._showTrace("开始时间最大时间差未指定, 自动设置为 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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from pages.services.CaptchaSolver import CaptchaSolver from .CaptchaSolver import CaptchaSolver
from pages.services.ReserveChecker import ReserveChecker from .ReserveChecker import ReserveChecker
from pages.services.RecordChecker import RecordChecker from .RecordChecker import RecordChecker
__all__ = [
"CaptchaSolver",
"ReserveChecker",
"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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from pages.strategies.TimeSelectMaker import ( from .TimeSelectMaker import (
TimeSelectMaker, TimeSelectMaker,
TimeDecisionMaker, TimeDecisionMaker,
TimeOptionReader, TimeOptionReader,
@@ -17,14 +17,3 @@ from pages.strategies.TimeSelectMaker import (
TimeSelectionResult, TimeSelectionResult,
TimeRangeResult, 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: This software is provided "as is", without any warranty of any kind.
- TimerUtils: Timer utils class for the AutoLibrary project. You may use, modify, and distribute this file under the terms of the MIT License.
- JSONReader: JSON reader class for the AutoLibrary project. See the LICENSE file for details.
- JSONWriter: JSON writer class for the AutoLibrary project.
""" """