[patch] 新增自动更新功能 模块 优化进度条显示 横幅图片资源上传

This commit is contained in:
2025-10-29 14:58:09 +08:00
parent 8f1b51ef4f
commit 8c0580d140
5 changed files with 545 additions and 199 deletions

View File

@@ -14,6 +14,7 @@ import time
import socket import socket
from pathlib import Path from pathlib import Path
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
import ctypes
class UpdateDownloader(QThread): class UpdateDownloader(QThread):
@@ -131,6 +132,59 @@ class UpdateDownloader(QThread):
self.is_cancelled = True 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): def install_update_and_restart(installer_path):
""" """
简化版更新安装文件替换方式不使用NSIS安装 简化版更新安装文件替换方式不使用NSIS安装
@@ -138,9 +192,10 @@ def install_update_and_restart(installer_path):
工作流程: 工作流程:
1. 将安装包移动到程序目录 1. 将安装包移动到程序目录
2. 创建替换脚本 2. 创建替换脚本
3. 脚本等待程序退出 3. 以管理员身份启动脚本提前弹出UAC
4. 脚本解压并替换文件 4. 脚本等待程序退出
5. 重启程序 5. 脚本执行静默安装
6. 重启程序
Args: Args:
installer_path: 安装包路径 installer_path: 安装包路径
@@ -187,16 +242,16 @@ def install_update_and_restart(installer_path):
exe_name exe_name
) )
# 启动更新脚本(独立进程) # 🔥 关键修复以管理员身份启动批处理脚本提前触发UAC
print(f"[Updater] 启动更新脚本: {update_script_path}") print(f"[Updater] 以管理员身份启动更新脚本: {update_script_path}")
subprocess.Popen( success = run_as_admin(update_script_path)
['cmd', '/c', update_script_path],
creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
close_fds=True
)
print(f"[Updater] ✅ 更新脚本已启动") if success:
return True print(f"[Updater] ✅ 更新脚本已以管理员身份启动")
return True
else:
print(f"[Updater] ❌ 用户取消了UAC授权更新已取消")
return False
except Exception as e: except Exception as e:
print(f"[Updater] ❌ 启动更新失败: {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) install_dir = os.path.abspath(install_dir)
script_content = f"""@echo off script_content = f"""@echo off
chcp 65001 > nul REM ShuiDi AI Assistant Auto Update Script
title 水滴AI客服智能助手 - 自动更新 REM Encoding: UTF-8 BOM
title ShuiDi AI Assistant - Auto Update
echo ============================================ echo ============================================
echo 水滴AI客服智能助手 - 自动更新 echo ShuiDi AI Assistant Auto Update
echo ============================================ echo ============================================
echo. echo.
REM 切换到程序目录 REM Change to program directory
cd /d "{install_dir}" cd /d "{install_dir}"
echo [INFO] 程序目录: {install_dir} echo INFO: Program directory: {install_dir}
echo. echo.
REM 等待主程序退出最多等待30秒 REM Wait for main program to exit (max 60 seconds)
echo [INFO] 等待主程序退出... echo INFO: Waiting for main program to exit...
set count=0 set count=0
:wait_main_exit :wait_main_exit
tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL
if NOT ERRORLEVEL 1 ( if NOT ERRORLEVEL 1 (
if %count% LSS 30 ( if %count% LSS 60 (
timeout /t 1 /nobreak > nul timeout /t 1 /nobreak > nul
set /a count+=1 set /a count+=1
goto wait_main_exit goto wait_main_exit
) else ( ) else (
echo [WARN] 主程序未正常退出,强制继续安装 echo WARN: Main program did not exit normally, forcing installation
) )
) )
echo [OK] 主程序已退出 echo OK: Main program exited
echo. echo.
REM 执行静默安装(在当前目录下) REM Execute NSIS silent installation
echo [INFO] 开始安装更新... echo INFO: Starting installation...
echo [INFO] 安装包: "{installer_path}" echo INFO: Installer: "{installer_path}"
echo [INFO] 目标目录: "{install_dir}" echo INFO: Target directory: "{install_dir}"
echo. echo.
REM 使用完整路径和引号避免空格问题 REM Execute installation (with admin rights from NSIS)
"{installer_path}" /S /D="{install_dir}" "{installer_path}" /S /D={install_dir}
REM 等待安装完成 REM Wait for installation to complete
echo [INFO] 等待安装完成... echo INFO: Waiting for installation to complete...
timeout /t 20 /nobreak > nul timeout /t 25 /nobreak > nul
REM 清理安装包 REM Cleanup installer
echo [INFO] 清理安装包... echo INFO: Cleaning up installer...
del /f /q "{installer_path}" 2>nul del /f /q "{installer_path}" 2>nul
REM 启动新版本程序 REM Start new version
echo [INFO] 正在启动新版本... echo INFO: Starting new version...
if exist "{install_dir}\\{exe_name}" ( if exist "{install_dir}\\{exe_name}" (
start "" "{install_dir}\\{exe_name}" start "" "{install_dir}\\{exe_name}"
echo [OK] ✅ 程序已启动 echo OK: Program started successfully
) else ( ) else (
echo [ERROR] ❌ 找不到程序文件: {exe_name} echo ERROR: Program file not found: {exe_name}
echo [INFO] 请手动启动程序 echo INFO: Please start program manually
pause pause
) )
REM 延迟后删除自己 REM Delete this script after delay
timeout /t 2 /nobreak > nul timeout /t 2 /nobreak > nul
del /f /q "%~f0" 2>nul del /f /q "%~f0" 2>nul
exit exit

View File

@@ -159,6 +159,9 @@ class NSISInstaller:
Unicode True Unicode True
# 🔥 请求管理员权限(解决静默安装时的"Access denied"问题)
RequestExecutionLevel admin
# 定义应用程序信息 # 定义应用程序信息
!define APP_NAME "{self.app_name}" !define APP_NAME "{self.app_name}"
!define APP_NAME_EN "{self.app_name_en}" !define APP_NAME_EN "{self.app_name_en}"

152
main.py
View File

@@ -1468,66 +1468,63 @@ class LoginWindow(QMainWindow):
try: try:
self.add_log(f"✅ 下载完成: {installer_path}", "SUCCESS") self.add_log(f"✅ 下载完成: {installer_path}", "SUCCESS")
# 更新进度对话框状态 # 🎯 更新进度对话框到UAC等待阶段不关闭对话框
progress_dialog.download_finished(installer_path) progress_dialog.download_finished(installer_path)
# 延迟1秒,让用户看到"下载完成"提示 # 延迟2秒,让用户看到"下载完成"提示,然后自动开始安装
QTimer.singleShot(1000, lambda: self.prepare_install(installer_path, progress_dialog)) QTimer.singleShot(2000, lambda: self.prepare_install(installer_path, progress_dialog))
except Exception as e: except Exception as e:
self.add_log(f"处理下载完成事件失败: {e}", "ERROR") self.add_log(f"处理下载完成事件失败: {e}", "ERROR")
def prepare_install(self, installer_path, progress_dialog): def prepare_install(self, installer_path, progress_dialog):
"""准备安装""" """准备安装(不再弹窗询问,直接开始)"""
try: try:
# 关闭进度对话框 # 🔥 不再关闭进度对话框,也不再弹窗询问,直接开始安装
progress_dialog.accept() self.add_log("开始执行自动安装流程", "INFO")
# 提示即将安装 from auto_updater import install_update_and_restart
reply = QMessageBox.information(
self,
"准备安装",
"新版本下载完成!\n\n"
"程序将自动退出并安装新版本,\n"
"安装完成后会自动重启。\n\n"
"是否立即安装?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes: # 🔥 启动静默安装带UAC提升
from auto_updater import install_update_and_restart result = install_update_and_restart(installer_path)
if result:
# ✅ 用户授权成功脚本已启动但在等待GUI退出
self.add_log("✅ 用户已授权UAC更新脚本已启动", "SUCCESS")
self.add_log("进度条将执行到100%后提示用户", "INFO")
# 启动静默安装(带自动重启) # 🎯 更新进度对话框到UAC授权完成阶段
if install_update_and_restart(installer_path): progress_dialog.uac_authorized()
self.add_log("✅ 更新安装已启动,程序即将退出", "SUCCESS")
self.add_log("程序将在安装完成后自动重启", "INFO") # 🔥 新增等待进度条执行到100%后再处理
# 通过定时器循环检查进度是否到100%
# 显示提示 self.progress_check_timer = QTimer()
QMessageBox.information(
self, def check_progress_complete():
"正在更新", if progress_dialog.restart_progress >= 100:
"程序即将退出并开始安装。\n\n" # 进度已到100%,停止检查并弹窗询问用户
"安装完成后会自动重启,请稍候...", self.progress_check_timer.stop()
QMessageBox.Ok self.show_update_complete_dialog(progress_dialog)
)
self.progress_check_timer.timeout.connect(check_progress_complete)
# 延迟1秒后退出让用户看到提示 self.progress_check_timer.start(1000) # 每1秒检查一次
QTimer.singleShot(1000, self.quit_for_update)
else:
self.add_log("❌ 启动安装失败", "ERROR")
QMessageBox.critical(
self,
"安装失败",
"无法启动安装程序,请手动运行安装包:\n\n" + installer_path,
QMessageBox.Ok
)
else: else:
self.add_log("用户取消安装", "INFO") # ⚠️ 用户取消了UAC授权或启动失败
QMessageBox.information( self.add_log("⚠️ 更新已取消用户取消UAC授权或启动失败", "WARNING")
# 关闭进度对话框
progress_dialog.reject()
# 显示友好提示
QMessageBox.warning(
self, self,
"安装包已保存", "更新已取消",
f"安装包已保存到:\n{installer_path}\n\n您可以稍后手动运行安装。", "未能启动自动更新。\n\n"
"可能原因:\n"
"• 您取消了管理员权限授权\n"
"• 系统安全策略限制\n\n"
f"安装包已保存到:\n{installer_path}\n\n"
"您可以稍后手动运行安装包进行更新。",
QMessageBox.Ok QMessageBox.Ok
) )
@@ -1535,6 +1532,9 @@ class LoginWindow(QMainWindow):
self.add_log(f"准备安装失败: {e}", "ERROR") self.add_log(f"准备安装失败: {e}", "ERROR")
import traceback import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
# 关闭进度对话框
progress_dialog.reject()
def on_download_error(self, error_msg, progress_dialog, download_url, latest_version): def on_download_error(self, error_msg, progress_dialog, download_url, latest_version):
"""下载失败处理""" """下载失败处理"""
@@ -1571,6 +1571,64 @@ class LoginWindow(QMainWindow):
except Exception as e: except Exception as e:
self.add_log(f"处理下载失败异常: {e}", "ERROR") 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): def quit_for_update(self):
"""为更新而退出程序""" """为更新而退出程序"""
self.add_log("正在退出程序以进行更新...", "INFO") self.add_log("正在退出程序以进行更新...", "INFO")

BIN
static/hengfu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

View File

@@ -1,80 +1,188 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
更新进度对话框 现代化圆形进度条更新对话框
显示下载进度、重试信息和状态提示 展示完整更新流程:下载 → UAC授权 → 安装 → 重启
""" """
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel, from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel,
QPushButton, QProgressBar, QHBoxLayout) QPushButton, QHBoxLayout, QWidget)
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty
from PyQt5.QtGui import QFont 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): class UpdateProgressDialog(QDialog):
""" """
更新下载进度对话框 现代化更新进度对话框
特性 更新流程阶段
- 实时显示下载进度 1. 下载安装包 (0-50%)
- 显示重试状态 2. 等待UAC授权 (50-60%)
- 支持用户取消 3. 执行安装 (60-90%)
- 现代化UI设计 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): def __init__(self, version, parent=None):
super().__init__(parent) super().__init__(parent)
self.version = version self.version = version
self.downloader = None self.downloader = None
self.current_stage = self.STAGE_DOWNLOAD
self.download_progress = 0 # 下载进度0-100
self.restart_progress = 0 # 重启阶段进度0-100用于外部检查
self.init_ui() self.init_ui()
def init_ui(self): def init_ui(self):
"""初始化UI""" """初始化UI"""
self.setWindowTitle("正在更新") self.setWindowTitle("正在更新")
self.setFixedWidth(450) self.setFixedSize(500, 600)
# 禁止关闭按钮(只能通过取消按钮关闭) # 禁止关闭按钮(只能通过取消按钮关闭)
self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
# 主布局
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setSpacing(15) layout.setSpacing(20)
layout.setContentsMargins(25, 20, 25, 20) layout.setContentsMargins(30, 30, 30, 30)
# 标题 # ============ 标题区域 ============
self.title_label = QLabel(f"正在下载 v{self.version}") self.title_label = QLabel(f"正在更新到 v{self.version}")
self.title_label.setFont(QFont('Microsoft YaHei', 12, QFont.Bold)) self.title_label.setFont(QFont('Microsoft YaHei', 16, QFont.Bold))
self.title_label.setAlignment(Qt.AlignCenter) self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
layout.addWidget(self.title_label) layout.addWidget(self.title_label)
# 进度条 # ============ 圆形进度条 ============
self.progress_bar = QProgressBar() self.circular_progress = CircularProgressBar()
self.progress_bar.setMinimum(0) layout.addWidget(self.circular_progress, alignment=Qt.AlignCenter)
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.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.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #666; font-size: 10px;") self.status_label.setStyleSheet("color: #7f8c8d; font-size: 11px;")
self.status_label.setMinimumHeight(20) self.status_label.setMinimumHeight(25)
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
# 重试信息标签(默认隐藏) # ============ 重试信息(默认隐藏)============
self.retry_label = QLabel("") self.retry_label = QLabel("")
self.retry_label.setAlignment(Qt.AlignCenter) self.retry_label.setAlignment(Qt.AlignCenter)
self.retry_label.setStyleSheet("color: #ff9800; font-size: 10px; font-weight: bold;") self.retry_label.setStyleSheet("color: #ff9800; font-size: 11px; font-weight: bold;")
self.retry_label.setMinimumHeight(20) self.retry_label.setMinimumHeight(25)
self.retry_label.hide() self.retry_label.hide()
layout.addWidget(self.retry_label) layout.addWidget(self.retry_label)
# 按钮布局 # ============ 按钮区域 ============
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
button_layout.addStretch() button_layout.addStretch()
self.cancel_button = QPushButton("取消") self.cancel_button = QPushButton("取消更新")
self.cancel_button.setFixedWidth(100) self.cancel_button.setFixedWidth(120)
self.cancel_button.setFixedHeight(36)
self.cancel_button.clicked.connect(self.cancel_download) self.cancel_button.clicked.connect(self.cancel_download)
button_layout.addWidget(self.cancel_button) button_layout.addWidget(self.cancel_button)
@@ -93,39 +201,26 @@ class UpdateProgressDialog(QDialog):
background: qlineargradient( background: qlineargradient(
x1:0, y1:0, x2:0, y2:1, x1:0, y1:0, x2:0, y2:1,
stop:0 #f8fafb, stop:0 #f8fafb,
stop:0.5 #f1f4f6,
stop:1 #e8ecef stop:1 #e8ecef
); );
} }
QLabel { QLabel {
background: transparent; 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 { QPushButton {
background: qlineargradient( background: qlineargradient(
x1:0, y1:0, x2:0, y2:1, x1:0, y1:0, x2:0, y2:1,
stop:0 #f5f5f5, stop:0 #f5f5f5,
stop:1 #e0e0e0 stop:1 #e0e0e0
); );
border: 1px solid #ccc; border: 2px solid #ccc;
border-radius: 6px; border-radius: 8px;
padding: 8px 20px; padding: 8px 20px;
font-size: 11px; font-size: 12px;
font-weight: bold;
font-family: 'Microsoft YaHei'; font-family: 'Microsoft YaHei';
color: #555;
} }
QPushButton:hover { QPushButton:hover {
background: qlineargradient( background: qlineargradient(
@@ -133,25 +228,40 @@ class UpdateProgressDialog(QDialog):
stop:0 #e8e8e8, stop:0 #e8e8e8,
stop:1 #d0d0d0 stop:1 #d0d0d0
); );
border: 1px solid #999; border: 2px solid #999;
} }
QPushButton:pressed { QPushButton:pressed {
background: #d0d0d0; background: #d0d0d0;
} }
QPushButton:disabled {
background: #f0f0f0;
color: #aaa;
border: 2px solid #ddd;
}
""") """)
def update_progress(self, downloaded, total): def update_progress(self, downloaded, total):
""" """
更新下载进度 更新下载进度阶段10-50%
Args: Args:
downloaded: 已下载字节数 downloaded: 已下载字节数
total: 总字节数 total: 总字节数
""" """
if self.current_stage != self.STAGE_DOWNLOAD:
return # 不在下载阶段,忽略
if total > 0: if total > 0:
# 计算百分比 # 计算下载百分比0-100
progress = int((downloaded / total) * 100) download_percent = int((downloaded / total) * 100)
self.progress_bar.setValue(progress) 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显示 # 转换为MB显示
downloaded_mb = downloaded / (1024 * 1024) 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.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: else:
# 无法获取总大小时显示不确定进度 # 无法获取总大小时显示不确定状态
self.progress_bar.setMaximum(0)
self.progress_bar.setFormat("正在下载...")
self.status_label.setText("正在下载,请稍候...") self.status_label.setText("正在下载,请稍候...")
def show_retry_info(self, current_retry, max_retries): 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.setText(f"⚠️ 下载失败,正在重试... ({current_retry}/{max_retries})")
self.retry_label.show() self.retry_label.show()
# 重置进度 # 重置下载进度
self.progress_bar.setValue(0) self.download_progress = 0
self.status_label.setText("等待重试...") self.circular_progress.setValue(0)
self.circular_progress.setText("0%")
self.status_label.setText("正在重试连接...")
# 更新标题 # 更新标题
self.title_label.setText(f"正在重试下载 v{self.version}") self.title_label.setText(f"正在重试下载 v{self.version}")
def download_finished(self, file_path): def download_finished(self, file_path):
""" """
下载完成 下载完成进入UAC授权阶段阶段250-60%
Args: Args:
file_path: 下载的文件路径 file_path: 下载的文件路径
""" """
self.title_label.setText(f"✅ 下载完成 v{self.version}") self.current_stage = self.STAGE_UAC
self.status_label.setText("下载完成,正在准备安装...")
self.status_label.setStyleSheet("color: #28a745; font-size: 10px; font-weight: bold;") # 更新进度到50%
self.progress_bar.setValue(100) self.circular_progress.setValue(50)
self.progress_bar.setFormat("100% (完成)") 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.cancel_button.setEnabled(False)
self.retry_label.hide() 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进入安装阶段阶段360-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):
"""
准备重启阶段490-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): def download_error(self, error_msg):
""" """
@@ -216,35 +433,23 @@ class UpdateProgressDialog(QDialog):
else: else:
display_msg = error_msg display_msg = error_msg
self.stage_label.setText("⚠️ 更新失败")
self.status_label.setText(display_msg) self.status_label.setText(display_msg)
self.status_label.setStyleSheet("color: #dc3545; font-size: 10px; font-weight: bold;") self.status_label.setStyleSheet("color: #dc3545; font-size: 11px; font-weight: bold;")
self.cancel_button.setText("关闭")
self.retry_label.hide()
# 进度条变红 self.cancel_button.setText("关闭")
self.progress_bar.setStyleSheet(""" self.cancel_button.setEnabled(True)
QProgressBar { self.retry_label.hide()
border: 2px solid #dc3545;
border-radius: 8px;
text-align: center;
height: 28px;
background: white;
}
QProgressBar::chunk {
background: #dc3545;
border-radius: 6px;
}
""")
def cancel_download(self): 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 from PyQt5.QtWidgets import QMessageBox
reply = QMessageBox.question( reply = QMessageBox.question(
self, self,
"确认取消", "确认取消",
"确定要取消下载吗?", "确定要取消更新吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes | QMessageBox.No,
QMessageBox.No QMessageBox.No
) )
@@ -252,11 +457,30 @@ class UpdateProgressDialog(QDialog):
if reply == QMessageBox.Yes: if reply == QMessageBox.Yes:
self.downloader.cancel() self.downloader.cancel()
self.downloader.wait() self.downloader.wait()
self.status_label.setText("下载已取消") self.status_label.setText("更新已取消")
self.reject() self.reject() # 会自动清理定时器
else: 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__": if __name__ == "__main__":
@@ -267,24 +491,28 @@ if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
# 创建测试对话框 # 创建测试对话框
dialog = UpdateProgressDialog("1.5.53") dialog = UpdateProgressDialog("1.5.55")
# 模拟下载进度
dialog.show() dialog.show()
# 测试进度更新 # 模拟完整更新流程
import time def simulate_update():
from PyQt5.QtCore import QTimer # 阶段1模拟下载 (0-50%)
def simulate_download():
def test_progress(): total_size = 100 * 1024 * 1024 # 100MB
for i in range(0, 101, 10): for i in range(0, 101, 5):
dialog.update_progress(i * 1024 * 1024, 100 * 1024 * 1024) downloaded = int(i * total_size / 100)
QApplication.processEvents() dialog.update_progress(downloaded, total_size)
time.sleep(0.2) 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_()) sys.exit(app.exec_())