[patch] 新增自动更新功能 模块 优化进度条显示 横幅图片资源上传
This commit is contained in:
135
auto_updater.py
135
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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
148
main.py
148
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 install_update_and_restart(installer_path):
|
||||
self.add_log("✅ 更新安装已启动,程序即将退出", "SUCCESS")
|
||||
self.add_log("程序将在安装完成后自动重启", "INFO")
|
||||
if result:
|
||||
# ✅ 用户授权成功,脚本已启动(但在等待GUI退出)
|
||||
self.add_log("✅ 用户已授权UAC,更新脚本已启动", "SUCCESS")
|
||||
self.add_log("进度条将执行到100%后提示用户", "INFO")
|
||||
|
||||
# 显示提示
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"正在更新",
|
||||
"程序即将退出并开始安装。\n\n"
|
||||
"安装完成后会自动重启,请稍候...",
|
||||
QMessageBox.Ok
|
||||
)
|
||||
# 🎯 更新进度对话框到UAC授权完成阶段
|
||||
progress_dialog.uac_authorized()
|
||||
|
||||
# 延迟1秒后退出(让用户看到提示)
|
||||
QTimer.singleShot(1000, self.quit_for_update)
|
||||
else:
|
||||
self.add_log("❌ 启动安装失败", "ERROR")
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"安装失败",
|
||||
"无法启动安装程序,请手动运行安装包:\n\n" + installer_path,
|
||||
QMessageBox.Ok
|
||||
)
|
||||
# 🔥 新增:等待进度条执行到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
|
||||
)
|
||||
|
||||
@@ -1536,6 +1533,9 @@ class LoginWindow(QMainWindow):
|
||||
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):
|
||||
"""下载失败处理"""
|
||||
try:
|
||||
@@ -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")
|
||||
|
||||
BIN
static/hengfu.jpg
Normal file
BIN
static/hengfu.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
452
update_dialog.py
452
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,28 +284,140 @@ 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 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) # 延迟
|
||||
|
||||
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)
|
||||
# 下载完成
|
||||
QTimer.singleShot(2000, lambda: dialog.download_finished("/tmp/test.exe"))
|
||||
|
||||
dialog.download_finished("/tmp/test.exe")
|
||||
# 阶段2:模拟UAC授权 (50-60%)
|
||||
QTimer.singleShot(5000, lambda: dialog.uac_authorized())
|
||||
|
||||
QTimer.singleShot(500, test_progress)
|
||||
simulate_download()
|
||||
|
||||
QTimer.singleShot(500, simulate_update)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user