Files
cmshoppe/app/gui/main_window.py
T
chengma df9da63841
Tests / Python 3.11 / Windows (push) Has been cancelled
feat: 接入远程订阅策略门禁 T-705
2026-07-28 15:42:19 +08:00

821 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Main GUI window."""
from __future__ import annotations
from PySide6.QtCore import QUrl
from PySide6.QtGui import QDesktopServices
from .. import client_policy, subscription
from ..version import display_name
from .activation_dialog import MembershipActivationDialog
from .tabs.accounts import AccountsTab
from .tabs.apply import ApplyTab
from .tabs.collect import CollectTab
from .tabs.generate import GenerateTab
from .tabs.product_suite import ProductSuiteTab
from .tabs.settings import SettingsTab
from .widgets import *
from .workers import SubscriptionCheckWorker
PREFERRED_WINDOW_SIZE = (1180, 760)
MIN_WINDOW_SIZE = (960, 640)
WINDOW_SCREEN_MARGIN = 40
# 仅供直接构造 MainWindow 的兼容入口使用;正式启动以远程策略为准。
SUBSCRIPTION_CHECK_ENABLED = True
SUBSCRIPTION_ENFORCEMENT_ENABLED = True
def _screen_available_geometry():
app = QApplication.instance()
if app is None:
return None
screen = app.primaryScreen()
if screen is None:
return None
return screen.availableGeometry()
def _bounded_dimension(available_size, preferred_size, minimum_size, margin):
if available_size <= 0:
return preferred_size
if available_size > margin * 2:
maximum = available_size - margin * 2
else:
maximum = available_size
if maximum >= minimum_size:
return max(minimum_size, min(preferred_size, maximum))
return max(1, min(preferred_size, maximum))
def _fit_and_center_window(
window,
available_geometry=None,
preferred_size=PREFERRED_WINDOW_SIZE,
minimum_size=MIN_WINDOW_SIZE,
margin=WINDOW_SCREEN_MARGIN,
):
available = available_geometry if available_geometry is not None else _screen_available_geometry()
if available is None or available.width() <= 0 or available.height() <= 0:
window.resize(*preferred_size)
return
width = _bounded_dimension(
available.width(),
preferred_size[0],
minimum_size[0],
margin,
)
height = _bounded_dimension(
available.height(),
preferred_size[1],
minimum_size[1],
margin,
)
window.resize(width, height)
x = available.x() + max(0, (available.width() - width) // 2)
y = available.y() + max(0, (available.height() - height) // 2)
max_x = available.x() + max(0, available.width() - width)
max_y = available.y() + max(0, available.height() - height)
window.move(min(max(x, available.x()), max_x), min(max(y, available.y()), max_y))
class MainWindow(QMainWindow):
"""Main application window with the fixed workflow tabs."""
def __init__(
self,
db_path=None,
config=None,
config_path=None,
ai_models_path=None,
startup_status="",
subscription_policy=None,
):
super().__init__()
self._initial_window_fit_applied_after_show = False
self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config
self.config_path = (
config_path
or self.config.get("config_path")
or appconfig.CONFIG_PATH
)
self.db_path = _database_path(db_path, self.config)
self.ai_models_path = (
ai_models_path
or self.config.get("ai_models_path")
or appconfig.ai_models_config_path(self.config)
)
self._client_policy = (
subscription_policy
if isinstance(subscription_policy, client_policy.ClientPolicy)
else client_policy.ClientPolicy(
policy_version=client_policy.POLICY_VERSION,
subscription_check_enabled=SUBSCRIPTION_CHECK_ENABLED,
subscription_enforcement_enabled=SUBSCRIPTION_ENFORCEMENT_ENABLED,
updated_at="",
source="compatibility",
)
)
self.setWindowTitle(display_name())
window_icon = app_icon()
if window_icon is not None:
self.setWindowIcon(window_icon)
_fit_and_center_window(self)
self.setStyleSheet(BUTTON_BASE_STYLE)
self._settings_tab_index = TAB_TITLES.index("设置")
self._last_tab_index = 0
self._reverting_tab_change = False
self._subscription_status = subscription.SubscriptionStatus(
subscription.STATUS_UNAVAILABLE
)
self._subscription_worker = None
self._subscription_thread = None
self._subscription_request_token = 0
self._subscription_closed = False
self._expired_subscription_notice_shown = False
self._subscription_notice_state = None
self._subscription_notice_states_seen = set()
self._activation_dialog = None
self._first_use_guide_shown = False
self.tabs = QTabWidget()
self.tabs.setObjectName("mainTabs")
self.tabs.setStyleSheet(TAB_STYLE)
self.tabs.currentChanged.connect(self._on_tab_changed)
for title in TAB_TITLES:
self.tabs.addTab(self._build_tab(title), title)
settings_tab = self._settings_tab()
if hasattr(settings_tab, "settingsSaved"):
settings_tab.settingsSaved.connect(self._on_settings_saved)
if hasattr(settings_tab, "subscriptionCheckRequested"):
settings_tab.subscriptionCheckRequested.connect(
self.begin_subscription_check
)
central = QWidget()
central_layout = QVBoxLayout(central)
central_layout.setContentsMargins(0, 0, 0, 0)
central_layout.setSpacing(0)
central_layout.addWidget(self.tabs, 1)
self.setCentralWidget(central)
self.show_status("就绪", level="muted")
if startup_status:
self.show_status(startup_status, level="success")
def showEvent(self, event):
super().showEvent(event)
if not self._initial_window_fit_applied_after_show:
_fit_and_center_window(self)
self._initial_window_fit_applied_after_show = True
def show_status(self, message, level="muted"):
color = _status_level_color(level)
self.statusBar().setStyleSheet(f"QStatusBar {{ color: {color}; }}")
self.statusBar().showMessage(str(message))
def _build_tab(self, title):
if title == "① 导入采集":
return CollectTab(
db_path=self.db_path,
config=self.config,
status_callback=self.show_status,
open_accounts_callback=lambda: self.open_accounts_tab(),
refresh_workflow_callback=lambda: self.refresh_task_tabs(),
)
if title == "② AI生成":
return GenerateTab(
db_path=self.db_path,
config=self.config,
config_path=self.config_path,
status_callback=self.show_status,
title_prompt_path=appconfig.title_prompt_path(self.config),
cover_prompts_dir=appconfig.cover_prompts_dir(self.config),
open_accounts_callback=lambda: self.open_accounts_tab(),
refresh_workflow_callback=lambda: self.refresh_task_tabs(),
subscription_preflight_callback=self.ensure_subscription_for_new_submit,
)
if title == "③ 更新蝦皮":
return ApplyTab(
db_path=self.db_path,
config=self.config,
status_callback=self.show_status,
open_accounts_callback=lambda: self.open_accounts_tab(),
open_settings_callback=lambda: self.open_settings_tab(),
refresh_workflow_callback=lambda: self.refresh_task_tabs(),
)
if title == "账号管理":
return AccountsTab(
db_path=self.db_path,
config=self.config,
status_callback=self.show_status,
)
if title == "设置":
tab = SettingsTab(
config=self.config,
config_path=self.config_path,
ai_models_path=self.ai_models_path,
status_callback=self.show_status,
)
if hasattr(tab, "set_subscription_policy_enabled"):
tab.set_subscription_policy_enabled(
self._client_policy.subscription_check_enabled
)
return tab
if title == "商品套图":
return ProductSuiteTab(
db_path=self.db_path,
config=self.config,
config_path=self.config_path,
status_callback=self.show_status,
subscription_preflight_callback=self.ensure_subscription_for_new_submit,
)
raise ValueError(f"未知主界面模块:{title}")
def refresh_task_tabs(self):
for index in range(self.tabs.count()):
widget = self.tabs.widget(index)
if hasattr(widget, "refresh_tasks"):
widget.refresh_tasks()
def _on_settings_saved(self, backend):
for index in range(self.tabs.count()):
widget = self.tabs.widget(index)
if hasattr(widget, "refresh_gateway_state"):
widget.refresh_gateway_state()
label = "自定义网关" if str(backend) == "direct" else "默认网关"
self.show_status("设置已保存,当前使用%s" % label, level="success")
self.begin_subscription_check()
@property
def subscription_status(self):
return self._subscription_status
@property
def subscription_policy(self):
return self._client_policy
def begin_subscription_check(self):
"""Refresh membership state without blocking the Qt GUI thread."""
self._set_membership_window_title()
if not self._client_policy.subscription_check_enabled:
self._set_product_access(True)
self._set_subscription_check_running(False)
self.show_status("服务端暂未启用会员检测", level="muted")
return
self._subscription_request_token += 1
token = self._subscription_request_token
if self._subscription_worker is not None:
self._subscription_worker.cancel()
self._set_product_access(
not self._client_policy.subscription_enforcement_enabled
)
self._set_subscription_check_running(True)
self.show_status("正在验证会员状态", level="info")
worker = SubscriptionCheckWorker(
config=self.config,
cmhub_config_path=self.config.get("cmhub_config_path"),
)
worker.finished.connect(
lambda payload, token=token: self._on_subscription_checked(
token,
payload,
)
)
worker.cancelled.connect(
lambda payload, token=token: self._on_subscription_checked(
token,
payload,
)
)
thread = run_worker(worker, thread_name="SubscriptionCheckWorker", start=False)
thread.finished.connect(lambda: self._forget_subscription_thread(thread))
self._subscription_worker = worker
self._subscription_thread = thread
thread.start()
def ensure_subscription_for_new_submit(self, action_name="生成"):
"""Return whether a new product request may start from the current UI."""
if (
not self._client_policy.subscription_check_enabled
or not self._client_policy.subscription_enforcement_enabled
):
return True
status = self._subscription_status
if status.allows_product_workflows:
return True
if self._subscription_thread is not None:
self.show_status("正在验证会员状态,请稍后再%s" % action_name, level="info")
return False
self.show_status("%s:%s" % (action_name, status.user_message), level="warning")
self.open_settings_tab()
return False
def _on_subscription_checked(self, token, payload):
if self._subscription_closed or token != self._subscription_request_token:
return
result = payload.get("subscription") if isinstance(payload, dict) else None
if not isinstance(result, subscription.SubscriptionStatus):
result = subscription.SubscriptionStatus(subscription.STATUS_UNAVAILABLE)
self._subscription_status = result
self._apply_subscription_status(result)
def _forget_subscription_thread(self, thread):
if self._subscription_thread is thread:
self._subscription_thread = None
self._subscription_worker = None
self._set_subscription_check_running(False)
def _apply_subscription_status(
self,
status,
*,
show_notice=True,
show_first_use_guide=True,
):
self._subscription_status = status
if status.allows_product_workflows:
self._expired_subscription_notice_shown = False
self._subscription_notice_state = None
self._subscription_notice_states_seen.clear()
if status.state == subscription.STATUS_ACTIVE:
expiry = subscription.format_expiry(status.expires_at)
self._set_membership_window_title(
status.account_name,
status.plan_name,
"有效至%s" % expiry,
)
elif status.state == subscription.STATUS_GRACE:
expiry = subscription.format_expiry(
status.grace_expires_at
) or subscription.format_expiry(status.expires_at)
self._set_membership_window_title(
status.account_name,
status.plan_name,
"宽限至%s" % expiry,
)
else:
self._set_membership_window_title()
access_allowed = (
status.allows_product_workflows
or not self._client_policy.subscription_enforcement_enabled
)
self._set_product_access(access_allowed)
if status.allows_product_workflows:
message = (
"会员状态已验证"
if self._client_policy.subscription_enforcement_enabled
else "会员状态已验证,当前处于观察模式"
)
self.show_status(message, level="success")
if (
self._client_policy.subscription_enforcement_enabled
and show_notice
):
self._show_subscription_notice_once(status)
if show_first_use_guide:
QTimer.singleShot(0, self._show_first_use_guide_if_pending)
return
if not self._client_policy.subscription_enforcement_enabled:
self.show_status(
"订阅观察:%s,当前不影响使用" % status.user_message,
level=self._subscription_level(status),
)
return
self.show_status(status.user_message, level=self._subscription_level(status))
if status.state == subscription.STATUS_NOT_CONFIGURED:
self.show_status("请先绑定会员账号", level="warning")
self._show_membership_activation()
return
if status.state == subscription.STATUS_KEY_INVALID:
self.open_settings_tab()
self._show_membership_activation()
if self._activation_dialog is not None:
self._activation_dialog.focus_api_key()
return
self.open_settings_tab()
if status.state == subscription.STATUS_EXPIRED:
self._show_expired_subscription_notice_once(status)
return
self._show_subscription_state_notice_once(status)
def _show_membership_activation(self):
if self._activation_dialog is not None:
self._activation_dialog.raise_()
self._activation_dialog.activateWindow()
return
dialog = MembershipActivationDialog(self)
self._activation_dialog = dialog
dialog.validationRequested.connect(self._begin_activation_validation)
dialog.memberCenterRequested.connect(self._open_activation_member_center)
dialog.finished.connect(
lambda result, current=dialog: self._on_activation_dialog_finished(
current,
result,
)
)
dialog.open()
def _begin_activation_validation(self, api_key):
dialog = self._activation_dialog
key = str(api_key or "").strip()
if dialog is None or not key:
return
self._subscription_request_token += 1
token = self._subscription_request_token
if self._subscription_worker is not None:
self._subscription_worker.cancel()
self._set_product_access(False)
self._set_subscription_check_running(True)
dialog.set_validating(True)
self.show_status("正在验证会员账号", level="info")
worker = SubscriptionCheckWorker(
config=self.config,
cmhub_config_path=self.config.get("cmhub_config_path"),
base_url=appconfig.cmhub_config(self.config).get("base_url"),
api_key=key,
)
worker.finished.connect(
lambda payload, token=token, key=key: self._on_activation_checked(
token,
key,
payload,
)
)
worker.cancelled.connect(
lambda payload, token=token, key=key: self._on_activation_checked(
token,
key,
payload,
)
)
thread = run_worker(worker, thread_name="MembershipActivationWorker", start=False)
thread.finished.connect(lambda: self._forget_subscription_thread(thread))
self._subscription_worker = worker
self._subscription_thread = thread
thread.start()
def _on_activation_checked(self, token, api_key, payload):
if self._subscription_closed or token != self._subscription_request_token:
return
dialog = self._activation_dialog
if dialog is None:
return
dialog.set_validating(False)
status = payload.get("subscription") if isinstance(payload, dict) else None
if not isinstance(status, subscription.SubscriptionStatus):
status = subscription.SubscriptionStatus(subscription.STATUS_UNAVAILABLE)
self._subscription_status = status
if not status.credentials_accepted:
self._set_product_access(False)
self._set_membership_window_title()
if status.state == subscription.STATUS_KEY_INVALID:
message = "会员 API Key 无效,请检查后重新输入。"
dialog.focus_api_key()
elif status.state == subscription.STATUS_NOT_CONFIGURED:
message = "请输入有效的会员 API Key。"
dialog.focus_api_key()
else:
message = "暂时无法验证会员账号,请检查网络后重试。"
dialog.set_status(message, level="danger")
self.show_status(message, level="warning")
return
try:
appconfig.save_cmhub_config(
{"api_key": str(api_key or "").strip()},
path=self.config.get("cmhub_config_path")
or appconfig.cmhub_config_file_path(self.config),
)
except (OSError, appconfig.ConfigError):
message = "会员账号已验证,但 API Key 保存失败,请检查数据目录后重试。"
dialog.set_status(message, level="danger")
self.show_status(message, level="danger")
return
settings_tab = self._settings_tab()
if hasattr(settings_tab, "refresh_default_gateway_credentials"):
settings_tab.refresh_default_gateway_credentials()
if not status.allows_product_workflows:
self._set_product_access(False)
self._set_membership_window_title()
message = "%s。请前往会员中心处理后重新验证。" % status.user_message
dialog.set_status(message, level="danger")
self.show_status(status.user_message, level=self._subscription_level(status))
return
self._save_first_use_state("pending")
if status.notice_id:
self._save_subscription_notice_id(status.notice_id)
self._apply_subscription_status(
status,
show_notice=False,
show_first_use_guide=False,
)
dialog.set_status("会员账号验证成功,正在进入工作区。", level="success")
dialog.accept_activation()
QTimer.singleShot(0, self._show_first_use_guide_if_pending)
def _on_activation_dialog_finished(self, dialog, result):
if self._activation_dialog is dialog:
self._activation_dialog = None
dialog.deleteLater()
if (
result != QDialog.Accepted
and not self._subscription_closed
and self._client_policy.subscription_enforcement_enabled
and not self._subscription_status.allows_product_workflows
):
self.close()
def _open_activation_member_center(self):
manage_url = str(self._subscription_status.manage_url or "").strip()
if not manage_url:
manage_url = subscription.safe_manage_url(
appconfig.DEFAULT_MEMBER_CENTER_URL,
appconfig.DEFAULT_CMHUB_BASE_URL,
)
if not manage_url:
message = "会员中心地址当前不可用,请稍后重试。"
if self._activation_dialog is not None:
self._activation_dialog.set_status(message, level="danger")
self.show_status(message, level="warning")
return
opened = QDesktopServices.openUrl(QUrl(manage_url))
if opened is False:
message = "无法打开会员中心,请检查系统默认浏览器后重试。"
if self._activation_dialog is not None:
self._activation_dialog.set_status(message, level="danger")
self.show_status(message, level="warning")
def _show_first_use_guide_if_pending(self):
if self._first_use_guide_shown:
return
if appconfig.first_use_guide_state(self.config) != "pending":
return
if not self._subscription_status.allows_product_workflows:
return
self._first_use_guide_shown = True
box = QMessageBox(self)
box.setIcon(QMessageBox.Information)
box.setWindowTitle("首次使用指引")
box.setText(
"接下来按以下顺序开始使用:\n\n"
"1. 添加蝦皮店铺账号\n"
"2. 启动 Chrome 并人工完成登录\n"
"3. 导入商品 Excel\n"
"4. 开始采集、AI 生成和更新蝦皮"
)
start_button = box.addButton("开始配置店铺", QMessageBox.AcceptRole)
later_button = box.addButton("稍后提醒", QMessageBox.RejectRole)
dismiss_button = box.addButton("不再提示", QMessageBox.ActionRole)
box.setDefaultButton(start_button)
box.exec()
clicked = box.clickedButton()
if clicked is start_button:
self._save_first_use_state("completed")
self.open_accounts_tab()
elif clicked is dismiss_button:
self._save_first_use_state("dismissed")
elif clicked is later_button:
self.show_status("可稍后从账号管理开始配置店铺", level="muted")
def _save_first_use_state(self, state):
try:
saved = appconfig.save_first_use_guide_state(
state,
path=self.config_path,
)
except (OSError, appconfig.ConfigError):
self.show_status("首次使用引导状态保存失败", level="warning")
return False
self._replace_runtime_config(saved)
return True
def _save_subscription_notice_id(self, notice_id):
try:
saved = appconfig.save_subscription_notice_id(
notice_id,
path=self.config_path,
)
except (OSError, appconfig.ConfigError):
return False
self._replace_runtime_config(saved)
return True
def _replace_runtime_config(self, saved):
self.config.clear()
self.config.update(saved)
@staticmethod
def _subscription_level(status):
if status.state in {subscription.STATUS_UNAVAILABLE, subscription.STATUS_KEY_INVALID}:
return "warning"
return "danger"
def _set_membership_window_title(
self,
account_name="",
plan_name="",
expiry_text="",
):
account = " ".join(str(account_name or "").split())
plan = " ".join(str(plan_name or "").split())
expiry = " ".join(str(expiry_text or "").split())
if account and plan and expiry:
self.setWindowTitle(
"%s | %s · %s · %s" % (display_name(), account, plan, expiry)
)
return
self.setWindowTitle(display_name())
def _set_product_access(self, enabled):
for index, title in enumerate(TAB_TITLES):
self.tabs.setTabEnabled(index, bool(enabled) or title == "设置")
def _set_subscription_check_running(self, running):
settings_tab = self._settings_tab()
if hasattr(settings_tab, "set_subscription_check_running"):
settings_tab.set_subscription_check_running(running)
def _show_expired_subscription_notice_once(self, status):
self._show_subscription_state_notice_once(status)
def _show_subscription_state_notice_once(self, status):
state = status.state
notice_state = (
subscription.STATUS_UNAVAILABLE
if state == subscription.STATUS_LEGACY
else state
)
if notice_state in self._subscription_notice_states_seen:
return
self._subscription_notice_states_seen.add(notice_state)
self._subscription_notice_state = notice_state
self._expired_subscription_notice_shown = (
notice_state == subscription.STATUS_EXPIRED
)
titles = {
subscription.STATUS_REQUIRED: "尚未开通会员套餐",
subscription.STATUS_EXPIRED: "会员套餐已过期",
subscription.STATUS_REVOKED: "会员套餐已失效",
subscription.STATUS_ACCOUNT_DISABLED: "会员账号当前不可用",
subscription.STATUS_UNAVAILABLE: "暂时无法验证会员状态",
}
messages = {
subscription.STATUS_REQUIRED: (
"当前账号尚未开通蝦皮圈会员套餐,业务功能已暂停。"
"请前往会员中心选择套餐,完成后重新检测会员状态。"
),
subscription.STATUS_EXPIRED: (
"当前账号的蝦皮圈会员已到期,业务功能已暂停。"
"请前往会员中心续费或更换套餐,完成后重新检测会员状态。"
),
subscription.STATUS_REVOKED: (
"当前账号的蝦皮圈会员套餐已失效,业务功能已暂停。"
"请前往会员中心查看套餐状态,处理后重新检测。"
),
subscription.STATUS_ACCOUNT_DISABLED: (
"当前会员账号暂时不可用,业务功能已暂停。"
"请前往会员中心查看账号状态或联系管理员处理。"
),
subscription.STATUS_UNAVAILABLE: (
"当前暂时无法验证会员状态,业务功能已暂停。"
"这不代表账号未付费,请检查网络或服务状态后重新检测。"
),
}
box = QMessageBox(self)
box.setIcon(QMessageBox.Warning)
box.setWindowTitle(titles.get(notice_state, "暂时无法验证会员状态"))
text = messages.get(
notice_state,
messages[subscription.STATUS_UNAVAILABLE],
)
manage_button = None
settings_button = None
manage_url = str(status.manage_url or "").strip()
if notice_state != subscription.STATUS_UNAVAILABLE:
manage_button = box.addButton(
"前往会员中心",
QMessageBox.ActionRole,
)
manage_button.setEnabled(bool(manage_url))
if manage_url:
box.setDefaultButton(manage_button)
else:
manage_button.setToolTip("会员中心地址当前不可用")
text += "\n\n会员中心地址当前不可用,请稍后重试。"
retry_button = box.addButton("重新检测", QMessageBox.AcceptRole)
if notice_state == subscription.STATUS_UNAVAILABLE:
settings_button = box.addButton("打开设置", QMessageBox.ActionRole)
box.setDefaultButton(retry_button)
exit_button = box.addButton("退出程序", QMessageBox.DestructiveRole)
box.setText(text)
box.exec()
clicked = box.clickedButton()
if manage_button is not None and clicked is manage_button and manage_url:
opened = QDesktopServices.openUrl(QUrl(manage_url))
if opened is False:
self.show_status(
"无法打开会员中心,请检查系统默认浏览器后重试",
level="warning",
)
elif clicked is retry_button:
self.begin_subscription_check()
elif settings_button is not None and clicked is settings_button:
self.open_settings_tab()
elif clicked is exit_button:
self.close()
def _show_subscription_notice_once(self, status):
notice_id = str(status.notice_id or "").strip()
if not notice_id or notice_id == appconfig.subscription_notice_id(self.config):
return
box = QMessageBox(self)
box.setIcon(QMessageBox.Information)
box.setWindowTitle("已启用会员订阅")
expiry = subscription.format_expiry(
status.grace_expires_at if status.state == subscription.STATUS_GRACE else status.expires_at
)
text = "已启用蝦皮圈会员订阅服务。当前账号:%s;套餐:%s。" % (
status.account_name,
status.plan_name,
)
if expiry:
text += "当前有效至%s。" % expiry
text += "请在到期前前往线上会员中心选择或续费套餐。"
box.setText(text)
manage_button = None
if status.manage_url:
manage_button = box.addButton("前往会员中心", QMessageBox.ActionRole)
close_button = box.addButton("关闭", QMessageBox.AcceptRole)
box.setDefaultButton(close_button)
box.exec()
if manage_button is not None and box.clickedButton() is manage_button:
QDesktopServices.openUrl(QUrl(status.manage_url))
self._save_subscription_notice_id(notice_id)
def _on_tab_changed(self, index):
if self._reverting_tab_change:
self._last_tab_index = index
self.show_status(f"当前:{self.tabs.tabText(index)}", level="muted")
return
previous = self._last_tab_index
if previous == self._settings_tab_index and index != previous:
if not self._confirm_leave_settings_tab():
self._reverting_tab_change = True
try:
self.tabs.setCurrentIndex(previous)
finally:
self._reverting_tab_change = False
self._last_tab_index = previous
self.show_status(f"当前:{self.tabs.tabText(previous)}", level="muted")
return
self._last_tab_index = index
self.show_status(f"当前:{self.tabs.tabText(index)}", level="muted")
def _settings_tab(self):
return self.tabs.widget(self._settings_tab_index)
def _confirm_leave_settings_tab(self):
settings_tab = self._settings_tab()
if not hasattr(settings_tab, "is_dirty") or not settings_tab.is_dirty():
return True
box = QMessageBox(self)
box.setWindowTitle("未保存更改")
box.setText("设置有未保存更改。要先保存再离开吗?")
save_button = box.addButton("保存", QMessageBox.AcceptRole)
discard_button = box.addButton("放弃", QMessageBox.DestructiveRole)
box.addButton("取消", QMessageBox.RejectRole)
box.setDefaultButton(save_button)
box.exec()
clicked = box.clickedButton()
if clicked is save_button:
return bool(settings_tab.save_app_settings())
if clicked is discard_button:
return bool(settings_tab.discard_unsaved_changes())
return False
def closeEvent(self, event):
if self._confirm_leave_settings_tab():
self._subscription_closed = True
if self._subscription_worker is not None:
self._subscription_worker.cancel()
event.accept()
return
event.ignore()
def open_accounts_tab(self):
self.tabs.setCurrentIndex(TAB_TITLES.index("账号管理"))
def open_settings_tab(self):
self.tabs.setCurrentIndex(TAB_TITLES.index("设置"))