From 8575de6ea52ac5481c61a2742fd543206cf07b03 Mon Sep 17 00:00:00 2001 From: haosicheng Date: Thu, 30 Oct 2025 16:54:24 +0800 Subject: [PATCH] =?UTF-8?q?[patch]=20=E4=BC=98=E5=8C=96=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E9=80=BB=E8=BE=91=20=E4=B8=8E=20=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_update_dialog.py | 344 ++++++++++++++++++++++++++++++++++++++++ main.py | 149 +++++++++++++---- 2 files changed, 459 insertions(+), 34 deletions(-) create mode 100644 custom_update_dialog.py diff --git a/custom_update_dialog.py b/custom_update_dialog.py new file mode 100644 index 0000000..67bff93 --- /dev/null +++ b/custom_update_dialog.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +自定义版本更新对话框 +美观的布局和用户体验 +""" + +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QPushButton, QTextEdit, QFrame) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QCursor + + +class UpdateNotificationDialog(QDialog): + """ + 版本更新提示对话框 + + 布局: + - 标题区 + - 更新内容区 + - 下载地址区(可复制) + - 按钮区(两行,美观布局) + """ + + # 用户选择(返回值) + CHOICE_UPDATE_NOW = 1 # 立即更新 + CHOICE_UPDATE_LATER = 2 # 稍后更新 + CHOICE_IGNORE = 3 # 忽略此版本 + + def __init__(self, version, download_url, update_content="", parent=None): + super().__init__(parent) + self.version = version + self.download_url = download_url + self.update_content = update_content + self.user_choice = None + + self.init_ui() + + def init_ui(self): + """初始化UI""" + self.setWindowTitle("版本更新") + self.setFixedWidth(550) + self.setMinimumHeight(400) + + # 禁用关闭按钮(只能通过按钮关闭) + self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint) + + # 主布局 + layout = QVBoxLayout() + layout.setSpacing(15) + layout.setContentsMargins(25, 20, 25, 20) + + # ============ 标题区 ============ + title_label = QLabel(f"🔔 发现新版本 v{self.version}") + title_label.setFont(QFont('Microsoft YaHei', 14, QFont.Bold)) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("color: #2c3e50; margin-bottom: 5px;") + layout.addWidget(title_label) + + # 分隔线 + line1 = QFrame() + line1.setFrameShape(QFrame.HLine) + line1.setStyleSheet("background: #ddd; max-height: 1px;") + layout.addWidget(line1) + + # ============ 更新内容区(可选)============ + # 🔥 只有在有更新内容时才显示 + if self.update_content and self.update_content.strip(): + content_label = QLabel("📝 更新内容:") + content_label.setFont(QFont('Microsoft YaHei', 10, QFont.Bold)) + content_label.setStyleSheet("color: #34495e;") + layout.addWidget(content_label) + + # 更新内容文本 + content_text = QTextEdit() + content_text.setReadOnly(True) + content_text.setMaximumHeight(100) + content_text.setStyleSheet(""" + QTextEdit { + border: 1px solid #ddd; + border-radius: 6px; + padding: 8px; + background: #f8f9fa; + font-size: 11px; + color: #555; + } + """) + content_text.setText(self.update_content) + layout.addWidget(content_text) + + # ============ 下载地址区 ============ + url_label = QLabel("📥 下载地址:") + url_label.setFont(QFont('Microsoft YaHei', 10, QFont.Bold)) + url_label.setStyleSheet("color: #34495e; margin-top: 5px;") + layout.addWidget(url_label) + + # 下载地址文本框(可选择复制) + self.url_text = QTextEdit() + self.url_text.setReadOnly(True) + self.url_text.setMaximumHeight(60) + self.url_text.setText(self.download_url) + self.url_text.setStyleSheet(""" + QTextEdit { + border: 2px solid #4285f4; + border-radius: 6px; + padding: 8px; + background: #e3f2fd; + font-size: 10px; + color: #1565c0; + font-family: 'Consolas', 'Courier New', monospace; + } + """) + # 全选文本,方便复制 + self.url_text.selectAll() + + layout.addWidget(self.url_text) + + # 复制提示 + copy_hint = QLabel("💡 提示:上方地址已自动选中,按 Ctrl+C 即可复制") + copy_hint.setFont(QFont('Microsoft YaHei', 8)) + copy_hint.setStyleSheet("color: #7f8c8d;") + layout.addWidget(copy_hint) + + # 分隔线 + line2 = QFrame() + line2.setFrameShape(QFrame.HLine) + line2.setStyleSheet("background: #ddd; max-height: 1px; margin-top: 10px;") + layout.addWidget(line2) + + # ============ 操作说明区 ============ + hint_label = QLabel( + "• 在线自动更新:自动下载并安装新版本\n" + "• 稍后更新:保留提示在状态栏,可随时点击更新\n" + "• 忽略此版本:本次运行不再提示此版本" + ) + hint_label.setFont(QFont('Microsoft YaHei', 9)) + hint_label.setStyleSheet("color: #666; margin: 5px 0;") + layout.addWidget(hint_label) + + # ============ 按钮区(两行布局)============ + # 第一行:主要操作 + button_row1 = QHBoxLayout() + button_row1.setSpacing(10) + + self.update_now_btn = QPushButton("🚀 在线自动更新") + self.update_now_btn.setFont(QFont('Microsoft YaHei', 11, QFont.Bold)) + self.update_now_btn.setMinimumHeight(42) + self.update_now_btn.setCursor(QCursor(Qt.PointingHandCursor)) + self.update_now_btn.clicked.connect(self.on_update_now) + + self.update_later_btn = QPushButton("⏰ 稍后更新") + self.update_later_btn.setFont(QFont('Microsoft YaHei', 11)) + self.update_later_btn.setMinimumHeight(42) + self.update_later_btn.setCursor(QCursor(Qt.PointingHandCursor)) + self.update_later_btn.clicked.connect(self.on_update_later) + + button_row1.addWidget(self.update_now_btn) + button_row1.addWidget(self.update_later_btn) + layout.addLayout(button_row1) + + # 第二行:次要操作 + button_row2 = QHBoxLayout() + button_row2.setSpacing(10) + + self.ignore_btn = QPushButton("🚫 忽略此版本") + self.ignore_btn.setFont(QFont('Microsoft YaHei', 10)) + self.ignore_btn.setMinimumHeight(38) + self.ignore_btn.setCursor(QCursor(Qt.PointingHandCursor)) + self.ignore_btn.clicked.connect(self.on_ignore) + + self.copy_btn = QPushButton("📋 复制下载地址") + self.copy_btn.setFont(QFont('Microsoft YaHei', 10)) + self.copy_btn.setMinimumHeight(38) + self.copy_btn.setCursor(QCursor(Qt.PointingHandCursor)) + self.copy_btn.clicked.connect(self.on_copy_url) + + button_row2.addWidget(self.ignore_btn) + button_row2.addWidget(self.copy_btn) + layout.addLayout(button_row2) + + self.setLayout(layout) + + # 应用样式 + self.apply_styles() + + def apply_styles(self): + """应用现代化样式""" + self.setStyleSheet(""" + QDialog { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #f8fafb, + stop:1 #e8ecef + ); + } + + /* 主要操作按钮(立即更新、稍后更新)*/ + QPushButton#updateNowBtn { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #4285f4, + stop:1 #1565c0 + ); + border: none; + color: white; + border-radius: 8px; + font-weight: bold; + } + + QPushButton#updateNowBtn:hover { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #5294f5, + stop:1 #1e74c1 + ); + } + + QPushButton#updateLaterBtn { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #ff9800, + stop:1 #f57c00 + ); + border: none; + color: white; + border-radius: 8px; + } + + QPushButton#updateLaterBtn:hover { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #ffa726, + stop:1 #fb8c00 + ); + } + + /* 次要操作按钮(忽略、复制)*/ + QPushButton { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #f5f5f5, + stop:1 #e0e0e0 + ); + border: 2px solid #ccc; + border-radius: 8px; + color: #555; + } + + QPushButton:hover { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #e8e8e8, + stop:1 #d0d0d0 + ); + border: 2px solid #999; + } + """) + + # 设置按钮对象名以应用特定样式 + self.update_now_btn.setObjectName("updateNowBtn") + self.update_later_btn.setObjectName("updateLaterBtn") + + def on_update_now(self): + """立即更新""" + self.user_choice = self.CHOICE_UPDATE_NOW + self.accept() + + def on_update_later(self): + """稍后更新""" + self.user_choice = self.CHOICE_UPDATE_LATER + self.accept() + + def on_ignore(self): + """忽略此版本""" + self.user_choice = self.CHOICE_IGNORE + self.accept() + + def on_copy_url(self): + """复制下载地址""" + from PyQt5.QtWidgets import QApplication, QMessageBox + + clipboard = QApplication.clipboard() + clipboard.setText(self.download_url) + + # 显示复制成功提示(不关闭主对话框) + QMessageBox.information( + self, + "复制成功", + f"✅ 下载地址已复制到剪贴板!\n\n" + f"您可以直接粘贴(Ctrl+V)到浏览器下载。", + QMessageBox.Ok + ) + + def get_user_choice(self): + """获取用户选择""" + return self.user_choice + + +if __name__ == "__main__": + # 测试对话框 + import sys + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + + print("=" * 60) + print("测试更新对话框") + print("=" * 60) + print("\n场景1:有更新内容") + + dialog = UpdateNotificationDialog( + version="1.5.64", + download_url="https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.64.exe", + update_content="• 修复了DY图片/视频上传问题\n• 优化了自动更新流程\n• 添加了圆形进度条" + ) + + dialog.exec_() + choice = dialog.get_user_choice() + + if choice == UpdateNotificationDialog.CHOICE_UPDATE_NOW: + print("\n用户选择:在线自动更新") + elif choice == UpdateNotificationDialog.CHOICE_UPDATE_LATER: + print("\n用户选择:稍后更新") + elif choice == UpdateNotificationDialog.CHOICE_IGNORE: + print("\n用户选择:忽略此版本") + else: + print("\n用户关闭了对话框") + + # 测试场景2:无更新内容 + print("\n" + "=" * 60) + print("场景2:无更新内容(应该隐藏更新内容区域)") + print("=" * 60) + + dialog2 = UpdateNotificationDialog( + version="1.5.65", + download_url="https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.65.exe", + update_content="" # 空内容 + ) + + dialog2.exec_() + + print("\n测试完成!") + sys.exit(0) + diff --git a/main.py b/main.py index de6cdbd..48d231e 100644 --- a/main.py +++ b/main.py @@ -84,6 +84,10 @@ class LoginWindow(QMainWindow): # 横幅相关 self.promo_banner = None self.banner_shadow = None # 阴影效果引用 + + # 🔥 版本更新提示状态管理 + self.notified_version = None # 已提示过的版本号(本次运行期间) + self.pending_update_info = None # 待更新的版本信息 {version, url, content} self.initUI() @@ -167,6 +171,11 @@ class LoginWindow(QMainWindow): self.status_label.setAlignment(Qt.AlignCenter) self.status_label.setFont(QFont('Microsoft YaHei', 9)) self.status_label.setStyleSheet("color: #95a5a6; margin-top: 2px;") + self.status_label.setCursor(Qt.ArrowCursor) # 默认鼠标样式 + + # 🔥 让状态标签可点击(用于更新提示) + self.status_label.mousePressEvent = self.on_status_label_clicked + main_layout.addWidget(self.status_label) # 添加最小间距(为横幅留出位置) @@ -1203,6 +1212,53 @@ class LoginWindow(QMainWindow): self.quit_application() event.accept() + def on_status_label_clicked(self, event): + """状态标签被点击时的处理""" + try: + # 如果有待更新信息,点击打开更新对话框 + if self.pending_update_info: + self.add_log("📱 用户点击状态标签,打开更新对话框", "INFO") + self.show_update_dialog_manual() + except Exception as e: + self.add_log(f"处理状态标签点击失败: {e}", "ERROR") + + def show_update_dialog_manual(self): + """手动打开更新对话框(用户点击状态标签时)""" + if not self.pending_update_info: + return + + version = self.pending_update_info['version'] + url = self.pending_update_info['url'] + content = self.pending_update_info.get('content', '') + + self.add_log(f"显示更新对话框: v{version}", "INFO") + self._show_update_dialog_internal(version, url, content) + + def update_status_label_for_pending_update(self, latest_version, download_url): + """更新状态标签显示待更新信息""" + try: + # 保存待更新信息 + update_content = self._get_update_content(latest_version) + self.pending_update_info = { + 'version': latest_version, + 'url': download_url, + 'content': update_content + } + + # 更新状态标签 + self.status_label.setText(f"🔔 新版本 v{latest_version} 可用(点击更新)") + self.status_label.setStyleSheet( + "color: #ff9800; background: rgba(255, 152, 0, 0.1); " + "border-radius: 12px; padding: 5px 10px; font-weight: bold; " + "cursor: pointer;" # 鼠标变手型 + ) + self.status_label.setCursor(Qt.PointingHandCursor) # 手型光标 + + self.add_log(f"✅ 状态标签已更新为待更新状态: v{latest_version}", "INFO") + + except Exception as e: + self.add_log(f"更新状态标签失败: {e}", "ERROR") + def setup_version_checker(self): """设置版本检查器的GUI回调""" try: @@ -1233,8 +1289,18 @@ class LoginWindow(QMainWindow): """显示更新对话框(信号槽函数,始终在主线程中执行)""" try: self.add_log(f"🎯 主线程收到更新信号: v{latest_version}", "INFO") - # 🔥 修改:调用新的trigger_update方法(支持自动更新) + + # 🔥 检查是否已经提示过这个版本 + if self.notified_version == latest_version: + self.add_log(f"ℹ️ 版本 {latest_version} 已提示过,本次运行不再弹窗", "INFO") + # 只更新状态标签,不弹窗 + self.update_status_label_for_pending_update(latest_version, download_url) + return + + # 🔥 首次提示这个版本,显示对话框 + self.add_log(f"🔔 首次提示版本 {latest_version},显示更新对话框", "INFO") self.trigger_update(download_url, latest_version) + except Exception as e: self.add_log(f"❌ 显示更新对话框失败: {e}", "ERROR") import traceback @@ -1325,7 +1391,23 @@ class LoginWindow(QMainWindow): return "" def trigger_update(self, download_url, latest_version): - """触发更新下载(支持自动更新和手动下载)""" + """触发更新提示(首次检测到新版本时调用)""" + try: + # 🔥 获取更新内容(如果没有则传空字符串,对话框会自动隐藏该区域) + update_content = self._get_update_content(latest_version) + if not update_content: + update_content = "" # 确保是空字符串而非None + + # 调用内部方法显示对话框 + self._show_update_dialog_internal(latest_version, download_url, update_content) + + except Exception as e: + self.add_log(f"触发更新失败: {e}", "ERROR") + import traceback + self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + + def _show_update_dialog_internal(self, latest_version, download_url, update_content): + """内部方法:显示更新对话框(使用自定义对话框)""" try: # 检查下载地址是否有效 if not download_url or download_url.strip() == "": @@ -1338,46 +1420,45 @@ class LoginWindow(QMainWindow): ) return - # 获取更新内容 - update_content = self._get_update_content(latest_version) - if update_content: - content_preview = update_content[:100] + "..." if len(update_content) > 100 else update_content - else: - content_preview = "(暂无更新说明)" + # 🔥 使用自定义对话框(美观布局) + from custom_update_dialog import UpdateNotificationDialog - # 🔥 新增:弹出三选一对话框(自动更新/手动下载/取消) - msg_box = QMessageBox(self) - msg_box.setWindowTitle("版本更新") - msg_box.setText(f"发现新版本 v{latest_version},是否更新?") - msg_box.setInformativeText( - f"更新内容:\n{content_preview}\n\n" - f"推荐使用「自动更新」,程序将自动下载并安装新版本。" + dialog = UpdateNotificationDialog( + version=latest_version, + download_url=download_url, + update_content=update_content, + parent=self ) - msg_box.setIcon(QMessageBox.Information) - # 添加三个按钮 - auto_button = msg_box.addButton("自动更新", QMessageBox.YesRole) - manual_button = msg_box.addButton("手动下载", QMessageBox.NoRole) - cancel_button = msg_box.addButton("取消", QMessageBox.RejectRole) - msg_box.setDefaultButton(auto_button) + dialog.exec_() + choice = dialog.get_user_choice() - msg_box.exec_() - clicked_button = msg_box.clickedButton() - - if clicked_button == auto_button: - # 🔥 自动更新:下载并安装 - self.add_log("用户选择自动更新", "INFO") + # 处理用户选择 + if choice == UpdateNotificationDialog.CHOICE_UPDATE_NOW: + # 🔥 立即更新:启动自动更新流程 + self.add_log("用户选择立即更新", "INFO") + self.notified_version = latest_version # 标记已提示 self.start_auto_update(download_url, latest_version) - elif clicked_button == manual_button: - # 🔥 手动下载:打开浏览器 - self.add_log("用户选择手动下载", "INFO") - self.start_manual_download(download_url, latest_version) + + elif choice == UpdateNotificationDialog.CHOICE_UPDATE_LATER: + # 🔥 稍后更新:更新状态标签,保留提示 + self.add_log("用户选择稍后更新", "INFO") + self.notified_version = latest_version # 标记已提示(避免重复弹窗) + self.update_status_label_for_pending_update(latest_version, download_url) + + elif choice == UpdateNotificationDialog.CHOICE_IGNORE: + # 🔥 忽略此版本:标记已提示,清除待更新信息 + self.add_log(f"用户忽略版本 {latest_version}", "INFO") + self.notified_version = latest_version # 标记已提示 + self.pending_update_info = None # 清除待更新信息 else: - # 取消更新 - self.add_log("用户取消更新", "INFO") + # 用户直接关闭对话框(等同于稍后更新) + self.add_log("用户关闭更新对话框", "INFO") + self.notified_version = latest_version + self.update_status_label_for_pending_update(latest_version, download_url) except Exception as e: - self.add_log(f"更新失败: {e}", "ERROR") + self.add_log(f"显示更新对话框失败: {e}", "ERROR") import traceback self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")