feat: 优化设置布局并支持管理端地址切换 (#130)

This commit is contained in:
chengma
2026-08-11 09:24:04 +08:00
parent 4ede126f58
commit cd2889d76c
9 changed files with 347 additions and 47 deletions
+14 -8
View File
@@ -11,6 +11,7 @@ from .settings_repository import SettingsRepository
CLIENT_ID_KEY = "admin.client_id" CLIENT_ID_KEY = "admin.client_id"
CLIENT_NAME_KEY = "admin.client_name" CLIENT_NAME_KEY = "admin.client_name"
ADMIN_BASE_URL_KEY = "admin.base_url"
def generate_client_device_id() -> str: def generate_client_device_id() -> str:
@@ -56,8 +57,12 @@ class CurrentClientService:
) )
return CurrentClientSettings(client_id, client_name) return CurrentClientSettings(client_id, client_name)
def save(self, client_name: str) -> CurrentClientSettings: def save(
"""保存名称;设备号不存在时生成,存在时保持不变。""" self,
client_name: str,
admin_base_url: Optional[str] = None,
) -> CurrentClientSettings:
"""原子保存身份;传入管理端地址时一并保存。"""
normalized_name = self._clean_text(client_name) normalized_name = self._clean_text(client_name)
if len(normalized_name) > 50: if len(normalized_name) > 50:
@@ -70,12 +75,13 @@ class CurrentClientService:
if not client_id: if not client_id:
raise ValueError("生成的设备号不能为空") raise ValueError("生成的设备号不能为空")
self._repository.set_many( values = {
{ CLIENT_ID_KEY: client_id,
CLIENT_ID_KEY: client_id, CLIENT_NAME_KEY: normalized_name,
CLIENT_NAME_KEY: normalized_name, }
} if admin_base_url is not None:
) values[ADMIN_BASE_URL_KEY] = self._clean_text(admin_base_url)
self._repository.set_many(values)
return CurrentClientSettings(client_id, normalized_name) return CurrentClientSettings(client_id, normalized_name)
@staticmethod @staticmethod
+9
View File
@@ -40,6 +40,15 @@ class HttpAdminGateway(AdminGateway):
parsed = urlparse(normalized_url) parsed = urlparse(normalized_url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc: if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("Admin 服务地址必须是有效的 http 或 https 地址") raise ValueError("Admin 服务地址必须是有效的 http 或 https 地址")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
"Admin 服务地址不能包含账号、密码、查询参数或片段"
)
if timeout_seconds <= 0: if timeout_seconds <= 0:
raise ValueError("请求超时必须大于 0 秒") raise ValueError("请求超时必须大于 0 秒")
+35
View File
@@ -173,6 +173,12 @@ class ClaimTaskWorker(QObject):
self._cancelled = True self._cancelled = True
@pyqtSlot(object)
def replace_gateway(self, gateway: AdminGateway) -> None:
"""当前命令结束后,为后续领取和提交切换 Admin Gateway。"""
self._gateway = gateway
@pyqtSlot() @pyqtSlot()
def run(self) -> None: def run(self) -> None:
"""兼容旧调用;新代码通过 ``run_selected`` 提交命令。""" """兼容旧调用;新代码通过 ``run_selected`` 提交命令。"""
@@ -416,6 +422,7 @@ class PDDTaskPageEvent(QObject):
_claimRunRequested = pyqtSignal(str) _claimRunRequested = pyqtSignal(str)
_claimReleaseRequested = pyqtSignal() _claimReleaseRequested = pyqtSignal()
_claimShutdownRequested = pyqtSignal() _claimShutdownRequested = pyqtSignal()
_claimGatewayChanged = pyqtSignal(object)
def __init__( def __init__(
self, self,
@@ -486,11 +493,13 @@ class PDDTaskPageEvent(QObject):
pass pass
settings = settings_repository or SettingsRepository() settings = settings_repository or SettingsRepository()
self._settings_repository = settings
self._client_service = CurrentClientService(settings) self._client_service = CurrentClientService(settings)
self._selected_android_device_service = SelectedAndroidDeviceService( self._selected_android_device_service = SelectedAndroidDeviceService(
settings settings
) )
self._claim_gateway = claim_gateway self._claim_gateway = claim_gateway
self._owns_claim_gateway = claim_gateway is None
self._claim_gateway_error = "" self._claim_gateway_error = ""
if self._claim_gateway is None: if self._claim_gateway is None:
base_url = settings.get("admin.base_url", DEFAULT_ADMIN_BASE_URL) base_url = settings.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
@@ -520,6 +529,31 @@ class PDDTaskPageEvent(QObject):
if application is not None: if application is not None:
application.aboutToQuit.connect(self.shutdown) application.aboutToQuit.connect(self.shutdown)
@pyqtSlot(str)
def update_admin_base_url(self, base_url: str) -> None:
"""让下一次领取、执行和上报统一使用新的 Admin 地址。"""
if self._closing or not self._owns_claim_gateway:
return
timeout_value = self._settings_repository.get(
"admin.request_timeout_seconds", 3.0
)
try:
timeout_seconds = float(timeout_value)
gateway = HttpAdminGateway(
base_url,
timeout_seconds=timeout_seconds,
client_id=self._client_service.load().client_id,
)
except (TypeError, ValueError) as exc:
self._claim_gateway_error = str(exc)
return
self._claim_gateway = gateway
self._claim_gateway_error = ""
if self._claim_worker is not None:
self._claimGatewayChanged.emit(gateway)
def load_initial_tasks(self) -> None: def load_initial_tasks(self) -> None:
"""应用启动后读取第一页本地任务。""" """应用启动后读取第一页本地任务。"""
@@ -1018,6 +1052,7 @@ class PDDTaskPageEvent(QObject):
self._claimRunRequested.connect(worker.run_selected) self._claimRunRequested.connect(worker.run_selected)
self._claimReleaseRequested.connect(worker.release_device) self._claimReleaseRequested.connect(worker.release_device)
self._claimShutdownRequested.connect(worker.shutdown_worker) self._claimShutdownRequested.connect(worker.shutdown_worker)
self._claimGatewayChanged.connect(worker.replace_gateway)
worker.taskSaved.connect(self._on_claimed_task_saved) worker.taskSaved.connect(self._on_claimed_task_saved)
worker.retryableFailed.connect(self._route_claim_retryable_failed) worker.retryableFailed.connect(self._route_claim_retryable_failed)
worker.failed.connect(self._route_claim_failed) worker.failed.connect(self._route_claim_failed)
+55 -26
View File
@@ -13,7 +13,7 @@ from typing import Iterable, Optional
from PyQt5.QtCore import QAbstractTableModel, QEvent, QModelIndex, Qt, pyqtSignal from PyQt5.QtCore import QAbstractTableModel, QEvent, QModelIndex, Qt, pyqtSignal
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QFormLayout, QGridLayout,
QHeaderView, QHeaderView,
QHBoxLayout, QHBoxLayout,
QLineEdit, QLineEdit,
@@ -243,6 +243,11 @@ class SettingsPage(QWidget):
self.deviceNameInput.setMaxLength(50) self.deviceNameInput.setMaxLength(50)
self.deviceNameInput.setAccessibleName("当前客户端设备名") self.deviceNameInput.setAccessibleName("当前客户端设备名")
self.adminBaseUrlInput = LineEdit(self)
self.adminBaseUrlInput.setPlaceholderText("http://127.0.0.1:8080")
self.adminBaseUrlInput.setClearButtonEnabled(True)
self.adminBaseUrlInput.setAccessibleName("Admin 管理端地址")
self.currentDeviceSaveButton = PushButton(FIF.SAVE, "保存", self) self.currentDeviceSaveButton = PushButton(FIF.SAVE, "保存", self)
self.currentDeviceSaveButton.setAccessibleName("保存当前设备信息") self.currentDeviceSaveButton.setAccessibleName("保存当前设备信息")
self.currentDeviceStatusLabel = CaptionLabel( self.currentDeviceStatusLabel = CaptionLabel(
@@ -373,16 +378,26 @@ class SettingsPage(QWidget):
titleLayout.addWidget(self.currentDeviceStatusLabel, 1) titleLayout.addWidget(self.currentDeviceStatusLabel, 1)
layout.addLayout(titleLayout) layout.addLayout(titleLayout)
form = QFormLayout() self.currentDeviceFormLayout = QGridLayout()
form.setHorizontalSpacing(16) self.currentDeviceFormLayout.setHorizontalSpacing(12)
form.setVerticalSpacing(12) self.currentDeviceFormLayout.setVerticalSpacing(12)
deviceIdLabel = CaptionLabel("设备号", card) self.currentDeviceFormLayout.setColumnStretch(1, 1)
deviceIdLabel.setBuddy(self.deviceIdInput) self.currentDeviceFormLayout.setColumnStretch(3, 1)
deviceNameLabel = CaptionLabel("设备名", card) self.deviceIdLabel = CaptionLabel("设备号", card)
deviceNameLabel.setBuddy(self.deviceNameInput) self.deviceIdLabel.setBuddy(self.deviceIdInput)
form.addRow(deviceIdLabel, self.deviceIdInput) self.deviceNameLabel = CaptionLabel("设备名", card)
form.addRow(deviceNameLabel, self.deviceNameInput) self.deviceNameLabel.setBuddy(self.deviceNameInput)
layout.addLayout(form) self.adminBaseUrlLabel = CaptionLabel("管理端地址", card)
self.adminBaseUrlLabel.setBuddy(self.adminBaseUrlInput)
self.currentDeviceFormLayout.addWidget(self.deviceIdLabel, 0, 0)
self.currentDeviceFormLayout.addWidget(self.deviceIdInput, 0, 1)
self.currentDeviceFormLayout.addWidget(self.deviceNameLabel, 0, 2)
self.currentDeviceFormLayout.addWidget(self.deviceNameInput, 0, 3)
self.currentDeviceFormLayout.addWidget(self.adminBaseUrlLabel, 1, 0)
self.currentDeviceFormLayout.addWidget(
self.adminBaseUrlInput, 1, 1, 1, 3
)
layout.addLayout(self.currentDeviceFormLayout)
commandLayout = QHBoxLayout() commandLayout = QHBoxLayout()
commandLayout.addStretch(1) commandLayout.addStretch(1)
@@ -434,21 +449,30 @@ class SettingsPage(QWidget):
titleLayout.addWidget(self.updateStatusLabel, 1) titleLayout.addWidget(self.updateStatusLabel, 1)
layout.addLayout(titleLayout) layout.addLayout(titleLayout)
form = QFormLayout() self.updateFormLayout = QGridLayout()
form.setHorizontalSpacing(16) self.updateFormLayout.setHorizontalSpacing(12)
form.setVerticalSpacing(12) self.updateFormLayout.setVerticalSpacing(12)
versionLabel = CaptionLabel("当前版本", card) # 输入区按约 45:55 分配,标签列只占自身需要的宽度。
manifestLabel = CaptionLabel("清单地址", card) self.updateFormLayout.setColumnStretch(1, 45)
usernameLabel = CaptionLabel("账号", card) self.updateFormLayout.setColumnStretch(3, 55)
passwordLabel = CaptionLabel("密码", card) self.versionLabel = CaptionLabel("当前版本", card)
manifestLabel.setBuddy(self.updateManifestUrlInput) self.manifestLabel = CaptionLabel("清单地址", card)
usernameLabel.setBuddy(self.updateUsernameInput) self.usernameLabel = CaptionLabel("账号", card)
passwordLabel.setBuddy(self.updatePasswordInput) self.passwordLabel = CaptionLabel("密码", card)
form.addRow(versionLabel, self.currentVersionLabel) self.manifestLabel.setBuddy(self.updateManifestUrlInput)
form.addRow(manifestLabel, self.updateManifestUrlInput) self.usernameLabel.setBuddy(self.updateUsernameInput)
form.addRow(usernameLabel, self.updateUsernameInput) self.passwordLabel.setBuddy(self.updatePasswordInput)
form.addRow(passwordLabel, self.updatePasswordInput) self.updateFormLayout.addWidget(self.versionLabel, 0, 0)
layout.addLayout(form) self.updateFormLayout.addWidget(self.currentVersionLabel, 0, 1, 1, 3)
self.updateFormLayout.addWidget(self.manifestLabel, 1, 0)
self.updateFormLayout.addWidget(
self.updateManifestUrlInput, 1, 1, 1, 3
)
self.updateFormLayout.addWidget(self.usernameLabel, 2, 0)
self.updateFormLayout.addWidget(self.updateUsernameInput, 2, 1)
self.updateFormLayout.addWidget(self.passwordLabel, 2, 2)
self.updateFormLayout.addWidget(self.updatePasswordInput, 2, 3)
layout.addLayout(self.updateFormLayout)
commandLayout = QHBoxLayout() commandLayout = QHBoxLayout()
commandLayout.addStretch(1) commandLayout.addStretch(1)
@@ -468,6 +492,11 @@ class SettingsPage(QWidget):
self.currentDeviceStatusLabel.setText(message) self.currentDeviceStatusLabel.setText(message)
def set_admin_base_url(self, base_url: str) -> None:
"""显示当前保存的 Admin 管理端地址。"""
self.adminBaseUrlInput.setText(base_url)
def set_android_devices(self, devices: Iterable[AndroidDeviceRow]) -> None: def set_android_devices(self, devices: Iterable[AndroidDeviceRow]) -> None:
"""显示后续 ADB 服务返回的设备列表。""" """显示后续 ADB 服务返回的设备列表。"""
+74 -12
View File
@@ -30,6 +30,7 @@ from .admin_gateway import (
ClientRegistrationGateway, ClientRegistrationGateway,
) )
from .current_client_service import ( from .current_client_service import (
ADMIN_BASE_URL_KEY,
CurrentClientService, CurrentClientService,
generate_client_device_id, generate_client_device_id,
) )
@@ -46,7 +47,7 @@ DEVICE_ID_PLACEHOLDER = "待生成"
class CurrentClientSaveWorker(QObject): class CurrentClientSaveWorker(QObject):
"""在线程中先保存本地身份,再登记到 Admin。""" """在线程中先保存本地身份,再登记到 Admin。"""
localSaved = pyqtSignal(str, str) localSaved = pyqtSignal(str, str, str)
localSaveFailed = pyqtSignal(str) localSaveFailed = pyqtSignal(str)
registrationSucceeded = pyqtSignal(str) registrationSucceeded = pyqtSignal(str)
registrationFailed = pyqtSignal(str) registrationFailed = pyqtSignal(str)
@@ -58,6 +59,7 @@ class CurrentClientSaveWorker(QObject):
gateway: Optional[ClientRegistrationGateway], gateway: Optional[ClientRegistrationGateway],
client_name: str, client_name: str,
capabilities: ClaimCapabilities, capabilities: ClaimCapabilities,
admin_base_url: str,
gateway_error: str = "", gateway_error: str = "",
): ):
super().__init__() super().__init__()
@@ -65,6 +67,7 @@ class CurrentClientSaveWorker(QObject):
self._gateway = gateway self._gateway = gateway
self._client_name = client_name self._client_name = client_name
self._capabilities = capabilities self._capabilities = capabilities
self._admin_base_url = admin_base_url
self._gateway_error = gateway_error self._gateway_error = gateway_error
self._cancelled = False self._cancelled = False
@@ -77,12 +80,19 @@ class CurrentClientSaveWorker(QObject):
def run(self) -> None: def run(self) -> None:
try: try:
try: try:
saved = self._service.save(self._client_name) saved = self._service.save(
self._client_name,
self._admin_base_url,
)
except Exception as exc: except Exception as exc:
self.localSaveFailed.emit(str(exc) or "无法写入本地数据库") self.localSaveFailed.emit(str(exc) or "无法写入本地数据库")
return return
self.localSaved.emit(saved.client_id, saved.client_name) self.localSaved.emit(
saved.client_id,
saved.client_name,
self._admin_base_url,
)
if self._cancelled: if self._cancelled:
return return
@@ -300,10 +310,11 @@ class SettingsPageEventBinder(QObject):
searchRequested = pyqtSignal() searchRequested = pyqtSignal()
convertWifiRequested = pyqtSignal(str) convertWifiRequested = pyqtSignal(str)
currentDeviceSaveRequested = pyqtSignal(str, str) currentDeviceSaveRequested = pyqtSignal(str, str, str)
saveRequested = pyqtSignal(str, str) saveRequested = pyqtSignal(str, str)
deleteRequested = pyqtSignal(str) deleteRequested = pyqtSignal(str)
androidDeviceConfigurationChanged = pyqtSignal(str) androidDeviceConfigurationChanged = pyqtSignal(str)
adminBaseUrlChanged = pyqtSignal(str)
def __init__( def __init__(
self, self,
@@ -353,6 +364,7 @@ class SettingsPageEventBinder(QObject):
) )
repository = settings_repository or SettingsRepository() repository = settings_repository or SettingsRepository()
self._settings_repository = repository
self.updateEventBinder = UpdateUiEventBinder( self.updateEventBinder = UpdateUiEventBinder(
page, page,
repository, repository,
@@ -365,6 +377,8 @@ class SettingsPageEventBinder(QObject):
repository repository
) )
self._admin_gateway = admin_gateway self._admin_gateway = admin_gateway
self._owns_admin_gateway = admin_gateway is None
self._pending_admin_gateway = None
self._gateway_error = "" self._gateway_error = ""
if self._admin_gateway is None: if self._admin_gateway is None:
base_url = repository.get("admin.base_url", DEFAULT_ADMIN_BASE_URL) base_url = repository.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
@@ -419,12 +433,20 @@ class SettingsPageEventBinder(QObject):
device_id = self._page.deviceIdInput.text().strip() device_id = self._page.deviceIdInput.text().strip()
device_name = self._page.deviceNameInput.text().strip() device_name = self._page.deviceNameInput.text().strip()
admin_base_url = self._page.adminBaseUrlInput.text().strip()
self._page.deviceNameInput.setText(device_name) self._page.deviceNameInput.setText(device_name)
self.currentDeviceSaveRequested.emit(device_id, device_name) self.currentDeviceSaveRequested.emit(
device_id,
device_name,
admin_base_url,
)
@pyqtSlot(str, str) @pyqtSlot(str, str, str)
def _start_save_current_device( def _start_save_current_device(
self, _displayed_device_id: str, device_name: str self,
_displayed_device_id: str,
device_name: str,
admin_base_url: str,
) -> None: ) -> None:
if ( if (
self._closing self._closing
@@ -436,6 +458,15 @@ class SettingsPageEventBinder(QObject):
return return
try: try:
timeout_value = self._settings_repository.get(
"admin.request_timeout_seconds", 3.0
)
timeout_seconds = float(timeout_value)
validated_gateway = HttpAdminGateway(
admin_base_url,
timeout_seconds=timeout_seconds,
)
normalized_base_url = admin_base_url.strip().rstrip("/")
device = self._selected_android_device() device = self._selected_android_device()
purchase_mode = ( purchase_mode = (
"live" "live"
@@ -452,10 +483,19 @@ class SettingsPageEventBinder(QObject):
supported_types=supported_types, supported_types=supported_types,
purchase_mode=purchase_mode, purchase_mode=purchase_mode,
) )
except ValueError as exc: except (TypeError, ValueError) as exc:
self._page.set_current_device_status(f"保存失败:{exc}") self._page.set_current_device_status(f"保存失败:{exc}")
self._page.adminBaseUrlInput.setFocus()
return return
pending_gateway = (
validated_gateway
if self._owns_admin_gateway
else self._admin_gateway
)
self._pending_admin_gateway = pending_gateway
self._page.set_admin_base_url(normalized_base_url)
self._current_device_busy = True self._current_device_busy = True
self._sync_button_state() self._sync_button_state()
self._page.set_current_device_status("正在保存本地设备信息…") self._page.set_current_device_status("正在保存本地设备信息…")
@@ -463,10 +503,11 @@ class SettingsPageEventBinder(QObject):
thread = QThread(self) thread = QThread(self)
worker = CurrentClientSaveWorker( worker = CurrentClientSaveWorker(
self._client_service, self._client_service,
self._admin_gateway, pending_gateway,
device_name, device_name,
capabilities, capabilities,
self._gateway_error, normalized_base_url,
"",
) )
worker.moveToThread(thread) worker.moveToThread(thread)
@@ -905,6 +946,15 @@ class SettingsPageEventBinder(QObject):
"""页面创建时恢复本地 Client 信息。""" """页面创建时恢复本地 Client 信息。"""
try: try:
base_url = self._settings_repository.get(
ADMIN_BASE_URL_KEY,
DEFAULT_ADMIN_BASE_URL,
)
displayed_base_url = (
base_url.strip()
if isinstance(base_url, str) and base_url.strip()
else DEFAULT_ADMIN_BASE_URL
)
saved = self._client_service.load() saved = self._client_service.load()
except Exception as exc: except Exception as exc:
self._page.set_current_device_status( self._page.set_current_device_status(
@@ -912,6 +962,7 @@ class SettingsPageEventBinder(QObject):
) )
return return
self._page.set_admin_base_url(displayed_base_url)
self._page.set_client_info(saved.client_id, saved.client_name) self._page.set_client_info(saved.client_id, saved.client_name)
if saved.client_id: if saved.client_id:
self._page.set_current_device_status("本地设备信息已加载") self._page.set_current_device_status("本地设备信息已加载")
@@ -1144,11 +1195,20 @@ class SettingsPageEventBinder(QObject):
self._refresh_registration_after_android_setting = False self._refresh_registration_after_android_setting = False
QTimer.singleShot(0, self._request_save_current_device) QTimer.singleShot(0, self._request_save_current_device)
@pyqtSlot(str, str) @pyqtSlot(str, str, str)
def _on_local_saved(self, client_id: str, client_name: str) -> None: def _on_local_saved(
self,
client_id: str,
client_name: str,
admin_base_url: str,
) -> None:
if self._closing: if self._closing:
return return
self._page.set_client_info(client_id, client_name) self._page.set_client_info(client_id, client_name)
self._page.set_admin_base_url(admin_base_url)
self._admin_gateway = self._pending_admin_gateway
self._gateway_error = ""
self.adminBaseUrlChanged.emit(admin_base_url)
self._page.set_current_device_status( self._page.set_current_device_status(
"本地已保存,正在登记到 Admin…" "本地已保存,正在登记到 Admin…"
) )
@@ -1158,6 +1218,7 @@ class SettingsPageEventBinder(QObject):
if self._closing: if self._closing:
return return
self._page.set_current_device_status(f"本地保存失败:{message}") self._page.set_current_device_status(f"本地保存失败:{message}")
self._pending_admin_gateway = None
@pyqtSlot(str) @pyqtSlot(str)
def _on_registration_succeeded(self, _registered_at: str) -> None: def _on_registration_succeeded(self, _registered_at: str) -> None:
@@ -1178,6 +1239,7 @@ class SettingsPageEventBinder(QObject):
self._worker = None self._worker = None
self._thread = None self._thread = None
self._current_device_busy = False self._current_device_busy = False
self._pending_admin_gateway = None
if not self._closing: if not self._closing:
self._sync_button_state() self._sync_button_state()
+5
View File
@@ -63,6 +63,8 @@ class MainWindow(FluentWindow):
self.pddTaskPage, self.pddTaskPage,
task_repository or TaskRepository(), task_repository or TaskRepository(),
self, self,
claim_gateway=admin_gateway,
settings_repository=settings_repository,
purchase_adapter_factory=create_u2_purchase_adapter, purchase_adapter_factory=create_u2_purchase_adapter,
live_purchase_adapter_factory=create_u2_live_purchase_adapter, live_purchase_adapter_factory=create_u2_live_purchase_adapter,
purchase_reconcile_factory=create_u2_purchase_reconcile_adapter, purchase_reconcile_factory=create_u2_purchase_reconcile_adapter,
@@ -74,6 +76,9 @@ class MainWindow(FluentWindow):
self.settingsPage.eventBinder.androidDeviceConfigurationChanged.connect( self.settingsPage.eventBinder.androidDeviceConfigurationChanged.connect(
self.pddTaskPageEvent.android_device_configuration_changed self.pddTaskPageEvent.android_device_configuration_changed
) )
self.settingsPage.eventBinder.adminBaseUrlChanged.connect(
self.pddTaskPageEvent.update_admin_base_url
)
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd") self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
self.addSubInterface( self.addSubInterface(
self.settingsPage, self.settingsPage,
+25
View File
@@ -1110,6 +1110,31 @@ class PDDTaskPageEventTest(unittest.TestCase):
window.close() window.close()
window.deleteLater() window.deleteLater()
def test_admin_base_url_change_reaches_persistent_claim_worker(self):
settings = self._saved_settings()
page = PDDTaskPage()
events = PDDTaskPageEvent(
page,
self.repository,
settings_repository=settings,
)
worker = events._ensure_claim_executor()
events.update_admin_base_url("http://192.168.0.9:8080/")
self.assertTrue(
wait_until(
self.app,
lambda: getattr(worker._gateway, "_base_url", "")
== "http://192.168.0.9:8080",
)
)
self.assertEqual(
getattr(events._claim_gateway, "_base_url", ""),
"http://192.168.0.9:8080",
)
events.shutdown()
page.deleteLater()
def test_admin_task_mapping_uses_explicit_real_field_names(self): def test_admin_task_mapping_uses_explicit_real_field_names(self):
local_task = admin_task_to_new_claimed_task( local_task = admin_task_to_new_claimed_task(
collect_admin_task("COL-8020a8729f111c15") collect_admin_task("COL-8020a8729f111c15")
+120
View File
@@ -19,6 +19,7 @@ from src.android_device_service import (
AndroidWifiConversionResult, AndroidWifiConversionResult,
) )
from src.mock_admin_gateway import MockAdminGateway from src.mock_admin_gateway import MockAdminGateway
from src.http_admin_gateway import DEFAULT_ADMIN_BASE_URL
from src.selected_android_device_service import SELECTED_ANDROID_SERIAL_KEY from src.selected_android_device_service import SELECTED_ANDROID_SERIAL_KEY
from src.settings_repository import SettingsRepository from src.settings_repository import SettingsRepository
from src.settings_ui import AndroidDeviceRow, SettingsPage from src.settings_ui import AndroidDeviceRow, SettingsPage
@@ -212,10 +213,129 @@ class SettingsPageEventTest(unittest.TestCase):
self.assertEqual(page.deviceIdInput.text(), "CLIENT-EXISTING") self.assertEqual(page.deviceIdInput.text(), "CLIENT-EXISTING")
self.assertEqual(page.deviceNameInput.text(), "办公室电脑") self.assertEqual(page.deviceNameInput.text(), "办公室电脑")
self.assertEqual(page.adminBaseUrlInput.text(), DEFAULT_ADMIN_BASE_URL)
self.assertEqual(page.currentDeviceStatusLabel.text(), "本地设备信息已加载") self.assertEqual(page.currentDeviceStatusLabel.text(), "本地设备信息已加载")
page.eventBinder.shutdown() page.eventBinder.shutdown()
page.deleteLater() page.deleteLater()
def test_compact_form_layout_keeps_related_fields_on_same_row(self):
page = SettingsPage(
settings_repository=self.repository,
admin_gateway=MockAdminGateway(),
)
self.assertEqual(
page.currentDeviceFormLayout.getItemPosition(
page.currentDeviceFormLayout.indexOf(page.deviceIdInput)
),
(0, 1, 1, 1),
)
self.assertEqual(
page.currentDeviceFormLayout.getItemPosition(
page.currentDeviceFormLayout.indexOf(page.deviceNameInput)
),
(0, 3, 1, 1),
)
self.assertEqual(
page.currentDeviceFormLayout.getItemPosition(
page.currentDeviceFormLayout.indexOf(page.adminBaseUrlInput)
),
(1, 1, 1, 3),
)
self.assertEqual(
page.updateFormLayout.getItemPosition(
page.updateFormLayout.indexOf(page.updateUsernameInput)
),
(2, 1, 1, 1),
)
self.assertEqual(
page.updateFormLayout.getItemPosition(
page.updateFormLayout.indexOf(page.updatePasswordInput)
),
(2, 3, 1, 1),
)
self.assertEqual(page.updateFormLayout.columnStretch(1), 45)
self.assertEqual(page.updateFormLayout.columnStretch(3), 55)
self.assertIs(page.deviceIdLabel.buddy(), page.deviceIdInput)
self.assertIs(page.adminBaseUrlLabel.buddy(), page.adminBaseUrlInput)
self.assertIs(page.usernameLabel.buddy(), page.updateUsernameInput)
self.assertIs(page.passwordLabel.buddy(), page.updatePasswordInput)
page.eventBinder.shutdown()
page.deleteLater()
def test_save_persists_normalized_admin_base_url_and_emits_change(self):
gateway = MockAdminGateway()
page = SettingsPage(
settings_repository=self.repository,
admin_gateway=gateway,
)
changed_urls = []
page.eventBinder.adminBaseUrlChanged.connect(changed_urls.append)
page.deviceNameInput.setText("仓库电脑")
page.adminBaseUrlInput.setText(" http://192.168.0.8:8080/ ")
page.currentDeviceSaveButton.click()
self._wait_until(lambda: page.eventBinder._thread is None)
expected = "http://192.168.0.8:8080"
self.assertEqual(self.repository.get("admin.base_url"), expected)
self.assertEqual(page.adminBaseUrlInput.text(), expected)
self.assertEqual(changed_urls, [expected])
self.assertEqual(gateway.registration_count, 1)
page.eventBinder.shutdown()
page.deleteLater()
def test_invalid_admin_base_url_does_not_save_any_current_device_field(self):
self.repository.set_many(
{
"admin.client_id": "CLIENT-EXISTING",
"admin.client_name": "原设备名",
"admin.base_url": "http://127.0.0.1:8080",
}
)
gateway = MockAdminGateway()
page = SettingsPage(
settings_repository=self.repository,
admin_gateway=gateway,
)
page.show()
self.app.processEvents()
page.deviceNameInput.setText("不应保存的新名称")
page.adminBaseUrlInput.setText("不是地址")
page.currentDeviceSaveButton.click()
self.app.processEvents()
self.assertEqual(self.repository.get("admin.client_name"), "原设备名")
self.assertEqual(
self.repository.get("admin.base_url"),
"http://127.0.0.1:8080",
)
self.assertEqual(gateway.registration_count, 0)
self.assertIn("Admin 服务地址必须", page.currentDeviceStatusLabel.text())
self.assertIs(self.app.focusWidget(), page.adminBaseUrlInput)
page.eventBinder.shutdown()
page.deleteLater()
def test_admin_base_url_with_credentials_is_not_saved(self):
gateway = MockAdminGateway()
page = SettingsPage(
settings_repository=self.repository,
admin_gateway=gateway,
)
page.adminBaseUrlInput.setText(
"http://operator@127.0.0.1:8080"
)
page.currentDeviceSaveButton.click()
self.app.processEvents()
self.assertIsNone(self.repository.get("admin.base_url"))
self.assertEqual(gateway.registration_count, 0)
self.assertIn("不能包含账号、密码", page.currentDeviceStatusLabel.text())
page.eventBinder.shutdown()
page.deleteLater()
def test_settings_page_has_no_manual_live_purchase_authorization(self): def test_settings_page_has_no_manual_live_purchase_authorization(self):
page = SettingsPage( page = SettingsPage(
settings_repository=self.repository, settings_repository=self.repository,
+10 -1
View File
@@ -266,6 +266,14 @@ class TaskTableModel(QAbstractTableModel):
设置页使用可滚动布局和 Fluent 设置卡片,分组如下。 设置页使用可滚动布局和 Fluent 设置卡片,分组如下。
### 当前设备
- 第一行依次显示设备号和设备名;设备号只读,设备名可以修改,两组输入随窗口宽度平均伸缩;
- 第二行显示“管理端地址”,对应 SQLite 中已有的 `admin.base_url`,没有保存值时显示默认地址;
- 点击“保存”前先验证地址;只接受有效的 HTTP/HTTPS 地址,不能包含账号、密码、查询参数或片段。验证失败时不保存设备号、设备名或地址,并把焦点放回地址输入框;
- 保存成功后立即用新地址登记 Client,后续任务领取、结果提交和失败提交也统一切换到新地址;已经发出的请求等待自身结束,不强制中断;
- 保存不新增数据库表,不增加心跳,也不连接或操作 Android 设备。
### Admin ### Admin
- 服务地址; - 服务地址;
@@ -311,7 +319,8 @@ class TaskTableModel(QAbstractTableModel):
### 软件更新 ### 软件更新
- 显示只读的当前版本; - 显示只读的当前版本;
- 清单地址、账号和密码分别使用带可见标签的单行输入框,密码框默认隐藏且不回填; - 清单地址独占一行;账号和密码在下一行并排显示,输入区域按约 45:55 自适应伸缩;
- 三个字段都有可见标签,密码框默认隐藏且不回填;
- “保存”把 URL/账号写入 SQLite,把密码写入 Windows 凭据管理器;已有密码时留空 - “保存”把 URL/账号写入 SQLite,把密码写入 Windows 凭据管理器;已有密码时留空
表示不修改,修改账号时必须重新输入密码; 表示不修改,修改账号时必须重新输入密码;
- “检查更新”只使用已经保存的配置;存在未保存修改时先提示保存;检查和下载期间 - “检查更新”只使用已经保存的配置;存在未保存修改时先提示保存;检查和下载期间