diff --git a/auto_updater.py b/auto_updater.py index 80d63f3..e464bcf 100644 --- a/auto_updater.py +++ b/auto_updater.py @@ -14,6 +14,7 @@ import time import socket from pathlib import Path from PyQt5.QtCore import QThread, pyqtSignal +import ctypes class UpdateDownloader(QThread): @@ -131,6 +132,59 @@ class UpdateDownloader(QThread): self.is_cancelled = True +def run_as_admin(script_path): + """ + 以管理员身份运行脚本(Windows UAC提升) + + 特点: + - 在GUI退出前弹出UAC提示 + - 用户点击"允许"后,脚本以管理员身份运行 + - 用户点击"取消",返回False + + Args: + script_path: 批处理脚本路径 + + Returns: + bool: True表示成功启动(用户点击允许),False表示失败或用户取消 + """ + try: + print(f"[RunAsAdmin] 请求管理员权限执行: {script_path}") + + # 使用 ShellExecute 以管理员身份运行 + # "runas" 参数会触发UAC提示 + ret = ctypes.windll.shell32.ShellExecuteW( + None, # hwnd: 父窗口句柄(None表示无父窗口) + "runas", # lpOperation: 以管理员身份运行 + "cmd.exe", # lpFile: 要执行的程序 + f'/c "{script_path}"', # lpParameters: 命令行参数 + None, # lpDirectory: 工作目录(None表示当前目录) + 0 # nShowCmd: 0=隐藏窗口 + ) + + # 返回值说明: + # >32: 成功 + # <=32: 失败(具体错误码) + # - 5: 用户拒绝UAC(点击"否") + # - 2: 文件未找到 + # - 其他: 其他错误 + + if ret > 32: + print(f"[RunAsAdmin] ✅ 成功:用户已授权,脚本正在以管理员身份运行") + return True + elif ret == 5: + print(f"[RunAsAdmin] ⚠️ 用户取消了UAC授权(返回码: {ret})") + return False + else: + print(f"[RunAsAdmin] ❌ 启动失败(返回码: {ret})") + return False + + except Exception as e: + print(f"[RunAsAdmin] ❌ 异常: {e}") + import traceback + traceback.print_exc() + return False + + def install_update_and_restart(installer_path): """ 简化版更新安装(文件替换方式,不使用NSIS安装) @@ -138,9 +192,10 @@ def install_update_and_restart(installer_path): 工作流程: 1. 将安装包移动到程序目录 2. 创建替换脚本 - 3. 脚本等待程序退出 - 4. 脚本解压并替换文件 - 5. 重启程序 + 3. 以管理员身份启动脚本(提前弹出UAC) + 4. 脚本等待程序退出 + 5. 脚本执行静默安装 + 6. 重启程序 Args: installer_path: 安装包路径 @@ -187,16 +242,16 @@ def install_update_and_restart(installer_path): exe_name ) - # 启动更新脚本(独立进程) - print(f"[Updater] 启动更新脚本: {update_script_path}") - subprocess.Popen( - ['cmd', '/c', update_script_path], - creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS, - close_fds=True - ) + # 🔥 关键修复:以管理员身份启动批处理脚本,提前触发UAC + print(f"[Updater] 以管理员身份启动更新脚本: {update_script_path}") + success = run_as_admin(update_script_path) - print(f"[Updater] ✅ 更新脚本已启动") - return True + if success: + print(f"[Updater] ✅ 更新脚本已以管理员身份启动") + return True + else: + print(f"[Updater] ❌ 用户取消了UAC授权,更新已取消") + return False except Exception as e: print(f"[Updater] ❌ 启动更新失败: {e}") @@ -231,65 +286,67 @@ def create_update_installer_script(installer_path, install_dir, exe_name): install_dir = os.path.abspath(install_dir) script_content = f"""@echo off -chcp 65001 > nul -title 水滴AI客服智能助手 - 自动更新 +REM ShuiDi AI Assistant Auto Update Script +REM Encoding: UTF-8 BOM + +title ShuiDi AI Assistant - Auto Update echo ============================================ -echo 水滴AI客服智能助手 - 自动更新 +echo ShuiDi AI Assistant Auto Update echo ============================================ echo. -REM 切换到程序目录 +REM Change to program directory cd /d "{install_dir}" -echo [INFO] 程序目录: {install_dir} +echo INFO: Program directory: {install_dir} echo. -REM 等待主程序退出(最多等待30秒) -echo [INFO] 等待主程序退出... +REM Wait for main program to exit (max 60 seconds) +echo INFO: Waiting for main program to exit... set count=0 :wait_main_exit tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL if NOT ERRORLEVEL 1 ( - if %count% LSS 30 ( + if %count% LSS 60 ( timeout /t 1 /nobreak > nul set /a count+=1 goto wait_main_exit ) else ( - echo [WARN] 主程序未正常退出,强制继续安装 + echo WARN: Main program did not exit normally, forcing installation ) ) -echo [OK] 主程序已退出 +echo OK: Main program exited echo. -REM 执行静默安装(在当前目录下) -echo [INFO] 开始安装更新... -echo [INFO] 安装包: "{installer_path}" -echo [INFO] 目标目录: "{install_dir}" +REM Execute NSIS silent installation +echo INFO: Starting installation... +echo INFO: Installer: "{installer_path}" +echo INFO: Target directory: "{install_dir}" echo. -REM 使用完整路径和引号避免空格问题 -"{installer_path}" /S /D="{install_dir}" +REM Execute installation (with admin rights from NSIS) +"{installer_path}" /S /D={install_dir} -REM 等待安装完成 -echo [INFO] 等待安装完成... -timeout /t 20 /nobreak > nul +REM Wait for installation to complete +echo INFO: Waiting for installation to complete... +timeout /t 25 /nobreak > nul -REM 清理安装包 -echo [INFO] 清理安装包... +REM Cleanup installer +echo INFO: Cleaning up installer... del /f /q "{installer_path}" 2>nul -REM 启动新版本程序 -echo [INFO] 正在启动新版本... +REM Start new version +echo INFO: Starting new version... if exist "{install_dir}\\{exe_name}" ( start "" "{install_dir}\\{exe_name}" - echo [OK] ✅ 程序已启动 + echo OK: Program started successfully ) else ( - echo [ERROR] ❌ 找不到程序文件: {exe_name} - echo [INFO] 请手动启动程序 + echo ERROR: Program file not found: {exe_name} + echo INFO: Please start program manually pause ) -REM 延迟后删除自己 +REM Delete this script after delay timeout /t 2 /nobreak > nul del /f /q "%~f0" 2>nul exit diff --git a/installer/build_installer.py b/installer/build_installer.py index 9ff42cb..846a30a 100644 --- a/installer/build_installer.py +++ b/installer/build_installer.py @@ -159,6 +159,9 @@ class NSISInstaller: Unicode True +# 🔥 请求管理员权限(解决静默安装时的"Access denied"问题) +RequestExecutionLevel admin + # 定义应用程序信息 !define APP_NAME "{self.app_name}" !define APP_NAME_EN "{self.app_name_en}" diff --git a/main.py b/main.py index 0101216..ce7aaf7 100644 --- a/main.py +++ b/main.py @@ -1468,66 +1468,63 @@ class LoginWindow(QMainWindow): try: self.add_log(f"✅ 下载完成: {installer_path}", "SUCCESS") - # 更新进度对话框状态 + # 🎯 更新进度对话框到UAC等待阶段(不关闭对话框) progress_dialog.download_finished(installer_path) - # 延迟1秒,让用户看到"下载完成"提示 - QTimer.singleShot(1000, lambda: self.prepare_install(installer_path, progress_dialog)) + # 延迟2秒,让用户看到"下载完成"提示,然后自动开始安装 + QTimer.singleShot(2000, lambda: self.prepare_install(installer_path, progress_dialog)) except Exception as e: self.add_log(f"处理下载完成事件失败: {e}", "ERROR") def prepare_install(self, installer_path, progress_dialog): - """准备安装""" + """准备安装(不再弹窗询问,直接开始)""" try: - # 关闭进度对话框 - progress_dialog.accept() + # 🔥 不再关闭进度对话框,也不再弹窗询问,直接开始安装 + self.add_log("开始执行自动安装流程", "INFO") - # 提示即将安装 - reply = QMessageBox.information( - self, - "准备安装", - "新版本下载完成!\n\n" - "程序将自动退出并安装新版本,\n" - "安装完成后会自动重启。\n\n" - "是否立即安装?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes - ) + from auto_updater import install_update_and_restart - if reply == QMessageBox.Yes: - from auto_updater import install_update_and_restart + # 🔥 启动静默安装(带UAC提升) + result = install_update_and_restart(installer_path) + + if result: + # ✅ 用户授权成功,脚本已启动(但在等待GUI退出) + self.add_log("✅ 用户已授权UAC,更新脚本已启动", "SUCCESS") + self.add_log("进度条将执行到100%后提示用户", "INFO") - # 启动静默安装(带自动重启) - if install_update_and_restart(installer_path): - self.add_log("✅ 更新安装已启动,程序即将退出", "SUCCESS") - self.add_log("程序将在安装完成后自动重启", "INFO") - - # 显示提示 - QMessageBox.information( - self, - "正在更新", - "程序即将退出并开始安装。\n\n" - "安装完成后会自动重启,请稍候...", - QMessageBox.Ok - ) - - # 延迟1秒后退出(让用户看到提示) - QTimer.singleShot(1000, self.quit_for_update) - else: - self.add_log("❌ 启动安装失败", "ERROR") - QMessageBox.critical( - self, - "安装失败", - "无法启动安装程序,请手动运行安装包:\n\n" + installer_path, - QMessageBox.Ok - ) + # 🎯 更新进度对话框到UAC授权完成阶段 + progress_dialog.uac_authorized() + + # 🔥 新增:等待进度条执行到100%后再处理 + # 通过定时器循环检查进度是否到100% + self.progress_check_timer = QTimer() + + def check_progress_complete(): + if progress_dialog.restart_progress >= 100: + # 进度已到100%,停止检查并弹窗询问用户 + self.progress_check_timer.stop() + self.show_update_complete_dialog(progress_dialog) + + self.progress_check_timer.timeout.connect(check_progress_complete) + self.progress_check_timer.start(1000) # 每1秒检查一次 else: - self.add_log("用户取消安装", "INFO") - QMessageBox.information( + # ⚠️ 用户取消了UAC授权,或启动失败 + self.add_log("⚠️ 更新已取消(用户取消UAC授权或启动失败)", "WARNING") + + # 关闭进度对话框 + progress_dialog.reject() + + # 显示友好提示 + QMessageBox.warning( self, - "安装包已保存", - f"安装包已保存到:\n{installer_path}\n\n您可以稍后手动运行安装。", + "更新已取消", + "未能启动自动更新。\n\n" + "可能原因:\n" + "• 您取消了管理员权限授权\n" + "• 系统安全策略限制\n\n" + f"安装包已保存到:\n{installer_path}\n\n" + "您可以稍后手动运行安装包进行更新。", QMessageBox.Ok ) @@ -1535,6 +1532,9 @@ class LoginWindow(QMainWindow): self.add_log(f"准备安装失败: {e}", "ERROR") import traceback self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + + # 关闭进度对话框 + progress_dialog.reject() def on_download_error(self, error_msg, progress_dialog, download_url, latest_version): """下载失败处理""" @@ -1571,6 +1571,64 @@ class LoginWindow(QMainWindow): except Exception as e: self.add_log(f"处理下载失败异常: {e}", "ERROR") + def show_update_complete_dialog(self, progress_dialog): + """显示更新完成对话框,询问是否立即启动""" + try: + self.add_log("🎉 更新流程已完成,准备提示用户", "SUCCESS") + + # 清理检查定时器 + if hasattr(self, 'progress_check_timer') and self.progress_check_timer: + self.progress_check_timer.stop() + self.progress_check_timer = None + + # 🔥 不立即关闭进度对话框,先弹窗询问用户 + # 弹窗询问用户 + reply = QMessageBox.question( + self, + "更新完成", + "🎉 恭喜!新版本安装成功!\n\n" + "是否立即启动新版本?\n\n" + "• 点击「是」:立即启动新版本\n" + "• 点击「否」:稍后手动启动", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + # 关闭进度对话框 + progress_dialog.accept() + + if reply == QMessageBox.Yes: + self.add_log("✅ 用户选择立即启动新版本", "INFO") + self.add_log("🚀 程序立即退出,让批处理脚本完成安装", "INFO") + # 立即退出,让批处理脚本启动新版本(无延迟) + QApplication.instance().quit() + else: + self.add_log("ℹ️ 用户选择稍后手动启动", "INFO") + # 用户选择稍后启动,显示提示后退出 + QMessageBox.information( + self, + "提示", + "程序即将退出。\n\n" + "您可以随时从开始菜单或桌面快捷方式\n" + "启动新版本的水滴AI客服智能助手。", + QMessageBox.Ok + ) + self.add_log("🚀 程序退出", "INFO") + # 立即退出(无延迟) + QApplication.instance().quit() + + except Exception as e: + self.add_log(f"❌ 显示更新完成对话框失败: {e}", "ERROR") + import traceback + self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + # 关闭进度对话框 + try: + progress_dialog.reject() + except: + pass + # 出错也要立即退出 + QApplication.instance().quit() + def quit_for_update(self): """为更新而退出程序""" self.add_log("正在退出程序以进行更新...", "INFO") diff --git a/static/hengfu.jpg b/static/hengfu.jpg new file mode 100644 index 0000000..cd78df1 Binary files /dev/null and b/static/hengfu.jpg differ diff --git a/update_dialog.py b/update_dialog.py index 7309ab1..6dd38a8 100644 --- a/update_dialog.py +++ b/update_dialog.py @@ -1,80 +1,188 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -更新进度对话框 -显示下载进度、重试信息和状态提示 +现代化圆形进度条更新对话框 +展示完整更新流程:下载 → UAC授权 → 安装 → 重启 """ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel, - QPushButton, QProgressBar, QHBoxLayout) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont + QPushButton, QHBoxLayout, QWidget) +from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty +from PyQt5.QtGui import QFont, QPainter, QColor, QPen, QLinearGradient, QConicalGradient +from PyQt5.QtCore import QRectF, QPointF +import math + + +class CircularProgressBar(QWidget): + """ + 自定义圆形进度条组件 + + 特性: + - 顺时针旋转动画 + - 渐变色圆环 + - 中心显示百分比 + - 支持平滑过渡 + """ + + def __init__(self, parent=None): + super().__init__(parent) + self._value = 0 # 当前进度值(0-100) + self._max_value = 100 + self._text = "0%" + self.setMinimumSize(200, 200) + + # 动画效果 + self.animation = QPropertyAnimation(self, b"value") + self.animation.setDuration(500) # 500ms过渡动画 + self.animation.setEasingCurve(QEasingCurve.OutCubic) + + @pyqtProperty(int) + def value(self): + return self._value + + @value.setter + def value(self, val): + self._value = val + self.update() # 触发重绘 + + def setValue(self, val): + """设置进度值(带动画)""" + if val != self._value: + self.animation.stop() + self.animation.setStartValue(self._value) + self.animation.setEndValue(val) + self.animation.start() + + def setText(self, text): + """设置中心文本""" + self._text = text + self.update() + + def paintEvent(self, event): + """绘制圆形进度条""" + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) # 抗锯齿 + + # 计算中心和半径 + width = self.width() + height = self.height() + size = min(width, height) + center = QPointF(width / 2, height / 2) + radius = size / 2 - 15 # 留出边距 + + # 绘制背景圆环(灰色) + pen = QPen() + pen.setWidth(12) + pen.setColor(QColor(230, 230, 230)) + pen.setCapStyle(Qt.RoundCap) + painter.setPen(pen) + + rect = QRectF(center.x() - radius, center.y() - radius, + radius * 2, radius * 2) + painter.drawArc(rect, 90 * 16, -360 * 16) # 绘制完整圆环 + + # 绘制进度圆环(渐变色) + if self._value > 0: + # 创建圆锥渐变(顺时针旋转效果) + gradient = QConicalGradient(center, 90) # 从顶部开始 + gradient.setColorAt(0.0, QColor(66, 133, 244)) # 蓝色 + gradient.setColorAt(0.5, QColor(25, 118, 210)) # 深蓝 + gradient.setColorAt(1.0, QColor(21, 101, 192)) # 更深蓝 + + pen.setBrush(gradient) + pen.setColor(QColor(66, 133, 244)) + painter.setPen(pen) + + # 计算进度角度(顺时针,从顶部开始) + span_angle = int(-(self._value / self._max_value) * 360 * 16) + painter.drawArc(rect, 90 * 16, span_angle) + + # 绘制中心文本(百分比) + painter.setPen(QColor(44, 62, 80)) + font = QFont('Microsoft YaHei', 28, QFont.Bold) + painter.setFont(font) + painter.drawText(rect, Qt.AlignCenter, self._text) class UpdateProgressDialog(QDialog): """ - 更新下载进度对话框 + 现代化更新进度对话框 - 特性: - - 实时显示下载进度 - - 显示重试状态 - - 支持用户取消 - - 现代化UI设计 + 更新流程阶段: + 1. 下载安装包 (0-50%) + 2. 等待UAC授权 (50-60%) + 3. 执行安装 (60-90%) + 4. 等待程序重启 (90-100%) """ + # 流程阶段定义 + STAGE_DOWNLOAD = 0 # 下载阶段 (0-50%) + STAGE_UAC = 1 # UAC授权阶段 (50-60%) + STAGE_INSTALL = 2 # 安装阶段 (60-90%) + STAGE_RESTART = 3 # 重启阶段 (90-100%) + def __init__(self, version, parent=None): super().__init__(parent) self.version = version self.downloader = None + self.current_stage = self.STAGE_DOWNLOAD + self.download_progress = 0 # 下载进度(0-100) + self.restart_progress = 0 # 重启阶段进度(0-100),用于外部检查 + self.init_ui() def init_ui(self): """初始化UI""" self.setWindowTitle("正在更新") - self.setFixedWidth(450) + self.setFixedSize(500, 600) # 禁止关闭按钮(只能通过取消按钮关闭) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint) + # 主布局 layout = QVBoxLayout() - layout.setSpacing(15) - layout.setContentsMargins(25, 20, 25, 20) + layout.setSpacing(20) + layout.setContentsMargins(30, 30, 30, 30) - # 标题 - self.title_label = QLabel(f"正在下载 v{self.version}") - self.title_label.setFont(QFont('Microsoft YaHei', 12, QFont.Bold)) + # ============ 标题区域 ============ + self.title_label = QLabel(f"正在更新到 v{self.version}") + self.title_label.setFont(QFont('Microsoft YaHei', 16, QFont.Bold)) self.title_label.setAlignment(Qt.AlignCenter) + self.title_label.setStyleSheet("color: #2c3e50; margin-bottom: 10px;") layout.addWidget(self.title_label) - # 进度条 - self.progress_bar = QProgressBar() - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(100) - self.progress_bar.setValue(0) - self.progress_bar.setTextVisible(True) - self.progress_bar.setFormat("%p%") - layout.addWidget(self.progress_bar) + # ============ 圆形进度条 ============ + self.circular_progress = CircularProgressBar() + layout.addWidget(self.circular_progress, alignment=Qt.AlignCenter) - # 状态文本 - self.status_label = QLabel("准备下载...") + # ============ 阶段提示 ============ + self.stage_label = QLabel("📥 正在下载安装包...") + self.stage_label.setFont(QFont('Microsoft YaHei', 12)) + self.stage_label.setAlignment(Qt.AlignCenter) + self.stage_label.setStyleSheet("color: #34495e; margin-top: 10px;") + layout.addWidget(self.stage_label) + + # ============ 详细状态 ============ + self.status_label = QLabel("准备开始下载...") self.status_label.setAlignment(Qt.AlignCenter) - self.status_label.setStyleSheet("color: #666; font-size: 10px;") - self.status_label.setMinimumHeight(20) + self.status_label.setStyleSheet("color: #7f8c8d; font-size: 11px;") + self.status_label.setMinimumHeight(25) layout.addWidget(self.status_label) - # 重试信息标签(默认隐藏) + # ============ 重试信息(默认隐藏)============ self.retry_label = QLabel("") self.retry_label.setAlignment(Qt.AlignCenter) - self.retry_label.setStyleSheet("color: #ff9800; font-size: 10px; font-weight: bold;") - self.retry_label.setMinimumHeight(20) + self.retry_label.setStyleSheet("color: #ff9800; font-size: 11px; font-weight: bold;") + self.retry_label.setMinimumHeight(25) self.retry_label.hide() layout.addWidget(self.retry_label) - # 按钮布局 + # ============ 按钮区域 ============ button_layout = QHBoxLayout() button_layout.addStretch() - self.cancel_button = QPushButton("取消") - self.cancel_button.setFixedWidth(100) + self.cancel_button = QPushButton("取消更新") + self.cancel_button.setFixedWidth(120) + self.cancel_button.setFixedHeight(36) self.cancel_button.clicked.connect(self.cancel_download) button_layout.addWidget(self.cancel_button) @@ -93,39 +201,26 @@ class UpdateProgressDialog(QDialog): background: qlineargradient( x1:0, y1:0, x2:0, y2:1, stop:0 #f8fafb, + stop:0.5 #f1f4f6, stop:1 #e8ecef ); } QLabel { background: transparent; } - QProgressBar { - border: 2px solid #ddd; - border-radius: 8px; - text-align: center; - height: 28px; - background: white; - font-size: 11px; - font-weight: bold; - } - QProgressBar::chunk { - background: qlineargradient(x1:0, y1:0, x2:1, y2:0, - stop:0 #4285f4, - stop:0.5 #1976d2, - stop:1 #1565c0); - border-radius: 6px; - } QPushButton { background: qlineargradient( x1:0, y1:0, x2:0, y2:1, stop:0 #f5f5f5, stop:1 #e0e0e0 ); - border: 1px solid #ccc; - border-radius: 6px; + border: 2px solid #ccc; + border-radius: 8px; padding: 8px 20px; - font-size: 11px; + font-size: 12px; + font-weight: bold; font-family: 'Microsoft YaHei'; + color: #555; } QPushButton:hover { background: qlineargradient( @@ -133,25 +228,40 @@ class UpdateProgressDialog(QDialog): stop:0 #e8e8e8, stop:1 #d0d0d0 ); - border: 1px solid #999; + border: 2px solid #999; } QPushButton:pressed { background: #d0d0d0; } + QPushButton:disabled { + background: #f0f0f0; + color: #aaa; + border: 2px solid #ddd; + } """) def update_progress(self, downloaded, total): """ - 更新下载进度 + 更新下载进度(阶段1:0-50%) Args: downloaded: 已下载字节数 total: 总字节数 """ + if self.current_stage != self.STAGE_DOWNLOAD: + return # 不在下载阶段,忽略 + if total > 0: - # 计算百分比 - progress = int((downloaded / total) * 100) - self.progress_bar.setValue(progress) + # 计算下载百分比(0-100) + download_percent = int((downloaded / total) * 100) + self.download_progress = download_percent + + # 映射到总进度的0-50% + overall_progress = int(download_percent * 0.5) + + # 更新圆形进度条 + self.circular_progress.setValue(overall_progress) + self.circular_progress.setText(f"{overall_progress}%") # 转换为MB显示 downloaded_mb = downloaded / (1024 * 1024) @@ -159,13 +269,8 @@ class UpdateProgressDialog(QDialog): # 更新状态文本 self.status_label.setText(f"已下载 {downloaded_mb:.1f} MB / {total_mb:.1f} MB") - - # 更新进度条格式 - self.progress_bar.setFormat(f"%p% ({downloaded_mb:.1f}/{total_mb:.1f} MB)") else: - # 无法获取总大小时显示不确定进度 - self.progress_bar.setMaximum(0) - self.progress_bar.setFormat("正在下载...") + # 无法获取总大小时显示不确定状态 self.status_label.setText("正在下载,请稍候...") def show_retry_info(self, current_retry, max_retries): @@ -179,27 +284,139 @@ class UpdateProgressDialog(QDialog): self.retry_label.setText(f"⚠️ 下载失败,正在重试... ({current_retry}/{max_retries})") self.retry_label.show() - # 重置进度条 - self.progress_bar.setValue(0) - self.status_label.setText("等待重试...") + # 重置下载进度 + self.download_progress = 0 + self.circular_progress.setValue(0) + self.circular_progress.setText("0%") + self.status_label.setText("正在重试连接...") # 更新标题 self.title_label.setText(f"正在重试下载 v{self.version}") def download_finished(self, file_path): """ - 下载完成 + 下载完成,进入UAC授权阶段(阶段2:50-60%) Args: file_path: 下载的文件路径 """ - self.title_label.setText(f"✅ 下载完成 v{self.version}") - self.status_label.setText("下载完成,正在准备安装...") - self.status_label.setStyleSheet("color: #28a745; font-size: 10px; font-weight: bold;") - self.progress_bar.setValue(100) - self.progress_bar.setFormat("100% (完成)") + self.current_stage = self.STAGE_UAC + + # 更新进度到50% + self.circular_progress.setValue(50) + self.circular_progress.setText("50%") + + # 更新阶段提示 + self.stage_label.setText("🔐 等待管理员授权...") + self.title_label.setText(f"✅ 下载完成") + self.status_label.setText("请在UAC窗口点击「允许」以继续...") + self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;") + + # 禁用取消按钮 self.cancel_button.setEnabled(False) self.retry_label.hide() + + # 启动UAC等待动画(50% → 60%) + self.start_uac_waiting_animation() + + def start_uac_waiting_animation(self): + """启动UAC等待动画(进度条在50-60%之间缓慢移动)""" + self.uac_timer = QTimer(self) + self.uac_progress = 50 + + def update_uac_progress(): + self.uac_progress += 0.5 # 每次增加0.5% + if self.uac_progress <= 60: + self.circular_progress.setValue(int(self.uac_progress)) + self.circular_progress.setText(f"{int(self.uac_progress)}%") + else: + # 超过60%停止(等待用户授权) + self.uac_timer.stop() + + self.uac_timer.timeout.connect(update_uac_progress) + self.uac_timer.start(500) # 每500ms更新一次 + + def uac_authorized(self): + """ + 用户已授权UAC,进入安装阶段(阶段3:60-90%) + """ + # 停止UAC等待动画 + if hasattr(self, 'uac_timer'): + self.uac_timer.stop() + + self.current_stage = self.STAGE_INSTALL + + # 更新进度到60% + self.circular_progress.setValue(60) + self.circular_progress.setText("60%") + + # 更新阶段提示 + self.stage_label.setText("⚙️ 正在安装新版本...") + self.title_label.setText("正在安装") + self.status_label.setText("正在替换程序文件,请稍候...") + self.status_label.setStyleSheet("color: #007bff; font-size: 11px; font-weight: bold;") + + # 启动安装进度动画(60% → 90%) + self.start_install_animation() + + def start_install_animation(self): + """启动安装进度动画(60% → 90%)""" + self.install_timer = QTimer(self) + self.install_progress = 60 + + def update_install_progress(): + self.install_progress += 1 # 每次增加1% + if self.install_progress <= 90: + self.circular_progress.setValue(self.install_progress) + self.circular_progress.setText(f"{self.install_progress}%") + else: + # 安装完成,进入重启阶段 + self.install_timer.stop() + self.prepare_restart() + + self.install_timer.timeout.connect(update_install_progress) + self.install_timer.start(800) # 每800ms增加1%(共24秒) + + def prepare_restart(self): + """ + 准备重启(阶段4:90-100%) + """ + self.current_stage = self.STAGE_RESTART + + # 更新进度到90% + self.circular_progress.setValue(90) + self.circular_progress.setText("90%") + + # 更新阶段提示 + self.stage_label.setText("🚀 准备启动新版本...") + self.title_label.setText("即将完成") + self.status_label.setText("程序即将重启,请稍候...") + self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;") + + # 启动重启倒计时动画(90% → 100%) + self.start_restart_animation() + + def start_restart_animation(self): + """启动重启倒计时动画(90% → 100%)""" + self.restart_timer = QTimer(self) + self.restart_progress = 90 + + def update_restart_progress(): + self.restart_progress += 2 # 每次增加2% + if self.restart_progress <= 100: + self.circular_progress.setValue(self.restart_progress) + self.circular_progress.setText(f"{self.restart_progress}%") + + if self.restart_progress == 100: + # 🔥 到达100%,更新提示但不自动关闭 + self.status_label.setText("✅ 安装完成!等待启动确认...") + self.stage_label.setText("🎉 更新成功!") + self.restart_timer.stop() # 停止定时器 + else: + self.restart_timer.stop() + + self.restart_timer.timeout.connect(update_restart_progress) + self.restart_timer.start(500) # 每500ms增加2%(共2.5秒) def download_error(self, error_msg): """ @@ -216,35 +433,23 @@ class UpdateProgressDialog(QDialog): else: display_msg = error_msg + self.stage_label.setText("⚠️ 更新失败") self.status_label.setText(display_msg) - self.status_label.setStyleSheet("color: #dc3545; font-size: 10px; font-weight: bold;") - self.cancel_button.setText("关闭") - self.retry_label.hide() + self.status_label.setStyleSheet("color: #dc3545; font-size: 11px; font-weight: bold;") - # 进度条变红 - self.progress_bar.setStyleSheet(""" - QProgressBar { - border: 2px solid #dc3545; - border-radius: 8px; - text-align: center; - height: 28px; - background: white; - } - QProgressBar::chunk { - background: #dc3545; - border-radius: 6px; - } - """) + self.cancel_button.setText("关闭") + self.cancel_button.setEnabled(True) + self.retry_label.hide() def cancel_download(self): """取消下载""" - if self.downloader and self.downloader.isRunning(): - # 确认取消 + if self.current_stage == self.STAGE_DOWNLOAD and self.downloader and self.downloader.isRunning(): + # 下载阶段可以取消 from PyQt5.QtWidgets import QMessageBox reply = QMessageBox.question( self, "确认取消", - "确定要取消下载吗?", + "确定要取消更新吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) @@ -252,11 +457,30 @@ class UpdateProgressDialog(QDialog): if reply == QMessageBox.Yes: self.downloader.cancel() self.downloader.wait() - self.status_label.setText("下载已取消") - self.reject() + self.status_label.setText("更新已取消") + self.reject() # 会自动清理定时器 else: - # 下载已完成或出错,直接关闭 - self.reject() + # 其他阶段或下载已完成,直接关闭 + self.reject() # 会自动清理定时器 + + def cleanup_timers(self): + """清理所有定时器""" + if hasattr(self, 'uac_timer'): + self.uac_timer.stop() + if hasattr(self, 'install_timer'): + self.install_timer.stop() + if hasattr(self, 'restart_timer'): + self.restart_timer.stop() + + def accept(self): + """对话框接受时清理资源""" + self.cleanup_timers() + super().accept() + + def reject(self): + """对话框拒绝时清理资源""" + self.cleanup_timers() + super().reject() if __name__ == "__main__": @@ -267,24 +491,28 @@ if __name__ == "__main__": app = QApplication(sys.argv) # 创建测试对话框 - dialog = UpdateProgressDialog("1.5.53") - - # 模拟下载进度 + dialog = UpdateProgressDialog("1.5.55") dialog.show() - # 测试进度更新 - import time - from PyQt5.QtCore import QTimer - - def test_progress(): - for i in range(0, 101, 10): - dialog.update_progress(i * 1024 * 1024, 100 * 1024 * 1024) - QApplication.processEvents() - time.sleep(0.2) + # 模拟完整更新流程 + def simulate_update(): + # 阶段1:模拟下载 (0-50%) + def simulate_download(): + total_size = 100 * 1024 * 1024 # 100MB + for i in range(0, 101, 5): + downloaded = int(i * total_size / 100) + dialog.update_progress(downloaded, total_size) + QApplication.processEvents() + QTimer.singleShot(100, lambda: None) # 延迟 + + # 下载完成 + QTimer.singleShot(2000, lambda: dialog.download_finished("/tmp/test.exe")) + + # 阶段2:模拟UAC授权 (50-60%) + QTimer.singleShot(5000, lambda: dialog.uac_authorized()) - dialog.download_finished("/tmp/test.exe") + simulate_download() - QTimer.singleShot(500, test_progress) + QTimer.singleShot(500, simulate_update) sys.exit(app.exec_()) -