feat: 优化设置布局并支持管理端地址切换 (#130)
This commit is contained in:
@@ -11,6 +11,7 @@ from .settings_repository import SettingsRepository
|
||||
|
||||
CLIENT_ID_KEY = "admin.client_id"
|
||||
CLIENT_NAME_KEY = "admin.client_name"
|
||||
ADMIN_BASE_URL_KEY = "admin.base_url"
|
||||
|
||||
|
||||
def generate_client_device_id() -> str:
|
||||
@@ -56,8 +57,12 @@ class CurrentClientService:
|
||||
)
|
||||
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)
|
||||
if len(normalized_name) > 50:
|
||||
@@ -70,12 +75,13 @@ class CurrentClientService:
|
||||
if not client_id:
|
||||
raise ValueError("生成的设备号不能为空")
|
||||
|
||||
self._repository.set_many(
|
||||
{
|
||||
values = {
|
||||
CLIENT_ID_KEY: client_id,
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -40,6 +40,15 @@ class HttpAdminGateway(AdminGateway):
|
||||
parsed = urlparse(normalized_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
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:
|
||||
raise ValueError("请求超时必须大于 0 秒")
|
||||
|
||||
|
||||
@@ -173,6 +173,12 @@ class ClaimTaskWorker(QObject):
|
||||
|
||||
self._cancelled = True
|
||||
|
||||
@pyqtSlot(object)
|
||||
def replace_gateway(self, gateway: AdminGateway) -> None:
|
||||
"""当前命令结束后,为后续领取和提交切换 Admin Gateway。"""
|
||||
|
||||
self._gateway = gateway
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self) -> None:
|
||||
"""兼容旧调用;新代码通过 ``run_selected`` 提交命令。"""
|
||||
@@ -416,6 +422,7 @@ class PDDTaskPageEvent(QObject):
|
||||
_claimRunRequested = pyqtSignal(str)
|
||||
_claimReleaseRequested = pyqtSignal()
|
||||
_claimShutdownRequested = pyqtSignal()
|
||||
_claimGatewayChanged = pyqtSignal(object)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -486,11 +493,13 @@ class PDDTaskPageEvent(QObject):
|
||||
pass
|
||||
|
||||
settings = settings_repository or SettingsRepository()
|
||||
self._settings_repository = settings
|
||||
self._client_service = CurrentClientService(settings)
|
||||
self._selected_android_device_service = SelectedAndroidDeviceService(
|
||||
settings
|
||||
)
|
||||
self._claim_gateway = claim_gateway
|
||||
self._owns_claim_gateway = claim_gateway is None
|
||||
self._claim_gateway_error = ""
|
||||
if self._claim_gateway is None:
|
||||
base_url = settings.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
|
||||
@@ -520,6 +529,31 @@ class PDDTaskPageEvent(QObject):
|
||||
if application is not None:
|
||||
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:
|
||||
"""应用启动后读取第一页本地任务。"""
|
||||
|
||||
@@ -1018,6 +1052,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._claimRunRequested.connect(worker.run_selected)
|
||||
self._claimReleaseRequested.connect(worker.release_device)
|
||||
self._claimShutdownRequested.connect(worker.shutdown_worker)
|
||||
self._claimGatewayChanged.connect(worker.replace_gateway)
|
||||
worker.taskSaved.connect(self._on_claimed_task_saved)
|
||||
worker.retryableFailed.connect(self._route_claim_retryable_failed)
|
||||
worker.failed.connect(self._route_claim_failed)
|
||||
|
||||
+55
-26
@@ -13,7 +13,7 @@ from typing import Iterable, Optional
|
||||
from PyQt5.QtCore import QAbstractTableModel, QEvent, QModelIndex, Qt, pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QHeaderView,
|
||||
QHBoxLayout,
|
||||
QLineEdit,
|
||||
@@ -243,6 +243,11 @@ class SettingsPage(QWidget):
|
||||
self.deviceNameInput.setMaxLength(50)
|
||||
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.setAccessibleName("保存当前设备信息")
|
||||
self.currentDeviceStatusLabel = CaptionLabel(
|
||||
@@ -373,16 +378,26 @@ class SettingsPage(QWidget):
|
||||
titleLayout.addWidget(self.currentDeviceStatusLabel, 1)
|
||||
layout.addLayout(titleLayout)
|
||||
|
||||
form = QFormLayout()
|
||||
form.setHorizontalSpacing(16)
|
||||
form.setVerticalSpacing(12)
|
||||
deviceIdLabel = CaptionLabel("设备号", card)
|
||||
deviceIdLabel.setBuddy(self.deviceIdInput)
|
||||
deviceNameLabel = CaptionLabel("设备名", card)
|
||||
deviceNameLabel.setBuddy(self.deviceNameInput)
|
||||
form.addRow(deviceIdLabel, self.deviceIdInput)
|
||||
form.addRow(deviceNameLabel, self.deviceNameInput)
|
||||
layout.addLayout(form)
|
||||
self.currentDeviceFormLayout = QGridLayout()
|
||||
self.currentDeviceFormLayout.setHorizontalSpacing(12)
|
||||
self.currentDeviceFormLayout.setVerticalSpacing(12)
|
||||
self.currentDeviceFormLayout.setColumnStretch(1, 1)
|
||||
self.currentDeviceFormLayout.setColumnStretch(3, 1)
|
||||
self.deviceIdLabel = CaptionLabel("设备号", card)
|
||||
self.deviceIdLabel.setBuddy(self.deviceIdInput)
|
||||
self.deviceNameLabel = CaptionLabel("设备名", card)
|
||||
self.deviceNameLabel.setBuddy(self.deviceNameInput)
|
||||
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.addStretch(1)
|
||||
@@ -434,21 +449,30 @@ class SettingsPage(QWidget):
|
||||
titleLayout.addWidget(self.updateStatusLabel, 1)
|
||||
layout.addLayout(titleLayout)
|
||||
|
||||
form = QFormLayout()
|
||||
form.setHorizontalSpacing(16)
|
||||
form.setVerticalSpacing(12)
|
||||
versionLabel = CaptionLabel("当前版本", card)
|
||||
manifestLabel = CaptionLabel("清单地址", card)
|
||||
usernameLabel = CaptionLabel("账号", card)
|
||||
passwordLabel = CaptionLabel("密码", card)
|
||||
manifestLabel.setBuddy(self.updateManifestUrlInput)
|
||||
usernameLabel.setBuddy(self.updateUsernameInput)
|
||||
passwordLabel.setBuddy(self.updatePasswordInput)
|
||||
form.addRow(versionLabel, self.currentVersionLabel)
|
||||
form.addRow(manifestLabel, self.updateManifestUrlInput)
|
||||
form.addRow(usernameLabel, self.updateUsernameInput)
|
||||
form.addRow(passwordLabel, self.updatePasswordInput)
|
||||
layout.addLayout(form)
|
||||
self.updateFormLayout = QGridLayout()
|
||||
self.updateFormLayout.setHorizontalSpacing(12)
|
||||
self.updateFormLayout.setVerticalSpacing(12)
|
||||
# 输入区按约 45:55 分配,标签列只占自身需要的宽度。
|
||||
self.updateFormLayout.setColumnStretch(1, 45)
|
||||
self.updateFormLayout.setColumnStretch(3, 55)
|
||||
self.versionLabel = CaptionLabel("当前版本", card)
|
||||
self.manifestLabel = CaptionLabel("清单地址", card)
|
||||
self.usernameLabel = CaptionLabel("账号", card)
|
||||
self.passwordLabel = CaptionLabel("密码", card)
|
||||
self.manifestLabel.setBuddy(self.updateManifestUrlInput)
|
||||
self.usernameLabel.setBuddy(self.updateUsernameInput)
|
||||
self.passwordLabel.setBuddy(self.updatePasswordInput)
|
||||
self.updateFormLayout.addWidget(self.versionLabel, 0, 0)
|
||||
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.addStretch(1)
|
||||
@@ -468,6 +492,11 @@ class SettingsPage(QWidget):
|
||||
|
||||
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:
|
||||
"""显示后续 ADB 服务返回的设备列表。"""
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from .admin_gateway import (
|
||||
ClientRegistrationGateway,
|
||||
)
|
||||
from .current_client_service import (
|
||||
ADMIN_BASE_URL_KEY,
|
||||
CurrentClientService,
|
||||
generate_client_device_id,
|
||||
)
|
||||
@@ -46,7 +47,7 @@ DEVICE_ID_PLACEHOLDER = "待生成"
|
||||
class CurrentClientSaveWorker(QObject):
|
||||
"""在线程中先保存本地身份,再登记到 Admin。"""
|
||||
|
||||
localSaved = pyqtSignal(str, str)
|
||||
localSaved = pyqtSignal(str, str, str)
|
||||
localSaveFailed = pyqtSignal(str)
|
||||
registrationSucceeded = pyqtSignal(str)
|
||||
registrationFailed = pyqtSignal(str)
|
||||
@@ -58,6 +59,7 @@ class CurrentClientSaveWorker(QObject):
|
||||
gateway: Optional[ClientRegistrationGateway],
|
||||
client_name: str,
|
||||
capabilities: ClaimCapabilities,
|
||||
admin_base_url: str,
|
||||
gateway_error: str = "",
|
||||
):
|
||||
super().__init__()
|
||||
@@ -65,6 +67,7 @@ class CurrentClientSaveWorker(QObject):
|
||||
self._gateway = gateway
|
||||
self._client_name = client_name
|
||||
self._capabilities = capabilities
|
||||
self._admin_base_url = admin_base_url
|
||||
self._gateway_error = gateway_error
|
||||
self._cancelled = False
|
||||
|
||||
@@ -77,12 +80,19 @@ class CurrentClientSaveWorker(QObject):
|
||||
def run(self) -> None:
|
||||
try:
|
||||
try:
|
||||
saved = self._service.save(self._client_name)
|
||||
saved = self._service.save(
|
||||
self._client_name,
|
||||
self._admin_base_url,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.localSaveFailed.emit(str(exc) or "无法写入本地数据库")
|
||||
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:
|
||||
return
|
||||
|
||||
@@ -300,10 +310,11 @@ class SettingsPageEventBinder(QObject):
|
||||
|
||||
searchRequested = pyqtSignal()
|
||||
convertWifiRequested = pyqtSignal(str)
|
||||
currentDeviceSaveRequested = pyqtSignal(str, str)
|
||||
currentDeviceSaveRequested = pyqtSignal(str, str, str)
|
||||
saveRequested = pyqtSignal(str, str)
|
||||
deleteRequested = pyqtSignal(str)
|
||||
androidDeviceConfigurationChanged = pyqtSignal(str)
|
||||
adminBaseUrlChanged = pyqtSignal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -353,6 +364,7 @@ class SettingsPageEventBinder(QObject):
|
||||
)
|
||||
|
||||
repository = settings_repository or SettingsRepository()
|
||||
self._settings_repository = repository
|
||||
self.updateEventBinder = UpdateUiEventBinder(
|
||||
page,
|
||||
repository,
|
||||
@@ -365,6 +377,8 @@ class SettingsPageEventBinder(QObject):
|
||||
repository
|
||||
)
|
||||
self._admin_gateway = admin_gateway
|
||||
self._owns_admin_gateway = admin_gateway is None
|
||||
self._pending_admin_gateway = None
|
||||
self._gateway_error = ""
|
||||
if self._admin_gateway is None:
|
||||
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_name = self._page.deviceNameInput.text().strip()
|
||||
admin_base_url = self._page.adminBaseUrlInput.text().strip()
|
||||
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(
|
||||
self, _displayed_device_id: str, device_name: str
|
||||
self,
|
||||
_displayed_device_id: str,
|
||||
device_name: str,
|
||||
admin_base_url: str,
|
||||
) -> None:
|
||||
if (
|
||||
self._closing
|
||||
@@ -436,6 +458,15 @@ class SettingsPageEventBinder(QObject):
|
||||
return
|
||||
|
||||
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()
|
||||
purchase_mode = (
|
||||
"live"
|
||||
@@ -452,10 +483,19 @@ class SettingsPageEventBinder(QObject):
|
||||
supported_types=supported_types,
|
||||
purchase_mode=purchase_mode,
|
||||
)
|
||||
except ValueError as exc:
|
||||
except (TypeError, ValueError) as exc:
|
||||
self._page.set_current_device_status(f"保存失败:{exc}")
|
||||
self._page.adminBaseUrlInput.setFocus()
|
||||
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._sync_button_state()
|
||||
self._page.set_current_device_status("正在保存本地设备信息…")
|
||||
@@ -463,10 +503,11 @@ class SettingsPageEventBinder(QObject):
|
||||
thread = QThread(self)
|
||||
worker = CurrentClientSaveWorker(
|
||||
self._client_service,
|
||||
self._admin_gateway,
|
||||
pending_gateway,
|
||||
device_name,
|
||||
capabilities,
|
||||
self._gateway_error,
|
||||
normalized_base_url,
|
||||
"",
|
||||
)
|
||||
worker.moveToThread(thread)
|
||||
|
||||
@@ -905,6 +946,15 @@ class SettingsPageEventBinder(QObject):
|
||||
"""页面创建时恢复本地 Client 信息。"""
|
||||
|
||||
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()
|
||||
except Exception as exc:
|
||||
self._page.set_current_device_status(
|
||||
@@ -912,6 +962,7 @@ class SettingsPageEventBinder(QObject):
|
||||
)
|
||||
return
|
||||
|
||||
self._page.set_admin_base_url(displayed_base_url)
|
||||
self._page.set_client_info(saved.client_id, saved.client_name)
|
||||
if saved.client_id:
|
||||
self._page.set_current_device_status("本地设备信息已加载")
|
||||
@@ -1144,11 +1195,20 @@ class SettingsPageEventBinder(QObject):
|
||||
self._refresh_registration_after_android_setting = False
|
||||
QTimer.singleShot(0, self._request_save_current_device)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _on_local_saved(self, client_id: str, client_name: str) -> None:
|
||||
@pyqtSlot(str, str, str)
|
||||
def _on_local_saved(
|
||||
self,
|
||||
client_id: str,
|
||||
client_name: str,
|
||||
admin_base_url: str,
|
||||
) -> None:
|
||||
if self._closing:
|
||||
return
|
||||
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(
|
||||
"本地已保存,正在登记到 Admin…"
|
||||
)
|
||||
@@ -1158,6 +1218,7 @@ class SettingsPageEventBinder(QObject):
|
||||
if self._closing:
|
||||
return
|
||||
self._page.set_current_device_status(f"本地保存失败:{message}")
|
||||
self._pending_admin_gateway = None
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_registration_succeeded(self, _registered_at: str) -> None:
|
||||
@@ -1178,6 +1239,7 @@ class SettingsPageEventBinder(QObject):
|
||||
self._worker = None
|
||||
self._thread = None
|
||||
self._current_device_busy = False
|
||||
self._pending_admin_gateway = None
|
||||
if not self._closing:
|
||||
self._sync_button_state()
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ class MainWindow(FluentWindow):
|
||||
self.pddTaskPage,
|
||||
task_repository or TaskRepository(),
|
||||
self,
|
||||
claim_gateway=admin_gateway,
|
||||
settings_repository=settings_repository,
|
||||
purchase_adapter_factory=create_u2_purchase_adapter,
|
||||
live_purchase_adapter_factory=create_u2_live_purchase_adapter,
|
||||
purchase_reconcile_factory=create_u2_purchase_reconcile_adapter,
|
||||
@@ -74,6 +76,9 @@ class MainWindow(FluentWindow):
|
||||
self.settingsPage.eventBinder.androidDeviceConfigurationChanged.connect(
|
||||
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.settingsPage,
|
||||
|
||||
@@ -1110,6 +1110,31 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
window.close()
|
||||
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):
|
||||
local_task = admin_task_to_new_claimed_task(
|
||||
collect_admin_task("COL-8020a8729f111c15")
|
||||
|
||||
@@ -19,6 +19,7 @@ from src.android_device_service import (
|
||||
AndroidWifiConversionResult,
|
||||
)
|
||||
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.settings_repository import SettingsRepository
|
||||
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.deviceNameInput.text(), "办公室电脑")
|
||||
self.assertEqual(page.adminBaseUrlInput.text(), DEFAULT_ADMIN_BASE_URL)
|
||||
self.assertEqual(page.currentDeviceStatusLabel.text(), "本地设备信息已加载")
|
||||
page.eventBinder.shutdown()
|
||||
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):
|
||||
page = SettingsPage(
|
||||
settings_repository=self.repository,
|
||||
|
||||
@@ -266,6 +266,14 @@ class TaskTableModel(QAbstractTableModel):
|
||||
|
||||
设置页使用可滚动布局和 Fluent 设置卡片,分组如下。
|
||||
|
||||
### 当前设备
|
||||
|
||||
- 第一行依次显示设备号和设备名;设备号只读,设备名可以修改,两组输入随窗口宽度平均伸缩;
|
||||
- 第二行显示“管理端地址”,对应 SQLite 中已有的 `admin.base_url`,没有保存值时显示默认地址;
|
||||
- 点击“保存”前先验证地址;只接受有效的 HTTP/HTTPS 地址,不能包含账号、密码、查询参数或片段。验证失败时不保存设备号、设备名或地址,并把焦点放回地址输入框;
|
||||
- 保存成功后立即用新地址登记 Client,后续任务领取、结果提交和失败提交也统一切换到新地址;已经发出的请求等待自身结束,不强制中断;
|
||||
- 保存不新增数据库表,不增加心跳,也不连接或操作 Android 设备。
|
||||
|
||||
### Admin
|
||||
|
||||
- 服务地址;
|
||||
@@ -311,7 +319,8 @@ class TaskTableModel(QAbstractTableModel):
|
||||
### 软件更新
|
||||
|
||||
- 显示只读的当前版本;
|
||||
- 清单地址、账号和密码分别使用带可见标签的单行输入框,密码框默认隐藏且不回填;
|
||||
- 清单地址独占一行;账号和密码在下一行并排显示,输入区域按约 45:55 自适应伸缩;
|
||||
- 三个字段都有可见标签,密码框默认隐藏且不回填;
|
||||
- “保存”把 URL/账号写入 SQLite,把密码写入 Windows 凭据管理器;已有密码时留空
|
||||
表示不修改,修改账号时必须重新输入密码;
|
||||
- “检查更新”只使用已经保存的配置;存在未保存修改时先提示保存;检查和下载期间
|
||||
|
||||
Reference in New Issue
Block a user