feat(client): add safe polling session UI

This commit is contained in:
QiuSW
2026-08-05 01:59:35 +08:00
parent 20112dbd61
commit dfd88c3336
14 changed files with 2253 additions and 17 deletions
@@ -0,0 +1,95 @@
"""采购工具固定双 Tab 原生窗口。"""
from __future__ import annotations
from PySide6.QtCore import QTimer, Qt, Slot
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import QMainWindow, QTabWidget
from cmbuyer_client.localstate.models import ProfileSettings
from cmbuyer_client.polling.coordinator import PollingCoordinator, PollingState, RecoveryStatus
from .execution import ExecutionPage
from .records import PurchaseRecordProvider
from .settings import ProfileStore, SettingsPage
class PurchaseToolWindow(QMainWindow):
EXECUTION_PAGE_ID = "purchase-execution"
SETTINGS_PAGE_ID = "settings"
def __init__(
self,
*,
store: ProfileStore,
coordinator: PollingCoordinator,
profile_settings: ProfileSettings | None,
has_stored_device_token: bool,
record_provider: PurchaseRecordProvider | None = None,
parent=None,
) -> None:
super().__init__(parent)
self.setWindowTitle("采购工具")
self.setAccessibleName("采购工具")
self.setMinimumSize(720, 520)
self.resize(1180, 760)
self._coordinator = coordinator
self._close_pending = False
recovery = coordinator.recovery_status
frozen = recovery is None or recovery.has_pending_claim or recovery.has_active_claim
self.tabs = QTabWidget()
self.tabs.setObjectName("mainTabs")
self.tabs.setTabsClosable(False)
self.tabs.setMovable(False)
self.execution_page = ExecutionPage(coordinator, record_provider)
self.execution_page.setProperty("pageId", self.EXECUTION_PAGE_ID)
self.settings_page = SettingsPage(
store,
settings=profile_settings,
has_stored_device_token=has_stored_device_token,
identity_frozen=frozen,
)
self.settings_page.setProperty("pageId", self.SETTINGS_PAGE_ID)
self.tabs.addTab(self.execution_page, "采购执行")
self.tabs.addTab(self.settings_page, "配置")
self.tabs.setCurrentWidget(self.execution_page)
self.setCentralWidget(self.tabs)
self.settings_page.settings_saved.connect(
lambda settings, has_token: self._coordinator.update_profile_settings(settings)
)
coordinator.recovery_status_changed.connect(self._on_recovery_status)
coordinator.configuration_freeze_changed.connect(self.settings_page.set_identity_frozen)
coordinator.settled.connect(self._finish_pending_close)
@Slot(object)
def _on_recovery_status(self, raw: object) -> None:
if not isinstance(raw, RecoveryStatus):
self.settings_page.set_identity_frozen(True)
return
self.settings_page.set_identity_frozen(raw.has_pending_claim or raw.has_active_claim)
def closeEvent(self, event: QCloseEvent) -> None:
running = self._coordinator.state in (
PollingState.STARTING,
PollingState.RECOVERING,
PollingState.WAITING,
PollingState.CLAIMING,
PollingState.ACTIVE,
) or self._coordinator.operation_in_flight
if running:
# 不 terminate 飞行中的 QThread。先提升 stop latch,等待 HTTP 自身
# 超时和 DurableClientGateway 落库,再由 settled 重试关闭。
self._close_pending = True
self._coordinator.stop()
event.ignore()
return
event.accept()
@Slot()
def _finish_pending_close(self) -> None:
if not self._close_pending:
return
self._close_pending = False
QTimer.singleShot(0, self.close)