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
+11 -5
View File
@@ -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
+9
View File
@@ -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 秒")
+35
View File
@@ -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
View File
@@ -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 服务返回的设备列表。"""
+74 -12
View File
@@ -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()
+5
View File
@@ -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,
+25
View File
@@ -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")
+120
View File
@@ -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,
+10 -1
View File
@@ -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 凭据管理器;已有密码时留空
表示不修改,修改账号时必须重新输入密码;
- “检查更新”只使用已经保存的配置;存在未保存修改时先提示保存;检查和下载期间