feat: 实现设置页设备管理界面 (#5)
This commit is contained in:
+298
-67
@@ -1,101 +1,332 @@
|
|||||||
"""设置页的界面结构。
|
"""设置页的界面结构。
|
||||||
|
|
||||||
分四组:Admin、Android 设备、自动化、安全与诊断。
|
本文件只负责显示设备信息和提供界面更新入口。按钮事件统一由
|
||||||
完整规范见 `docs/client/05-ui-specification.md` §8。
|
``settings_ui_event.py`` 绑定;ADB、SQLite 和硬件信息读取不得放在这里。
|
||||||
|
|
||||||
改动本文件前必读 `client/AGENTS.md`。以下几条最容易踩:
|
改动本文件前必读 ``client/AGENTS.md``。页面 ``objectName`` 固定为
|
||||||
|
``settingsPage``,图标使用 ``FluentIcon``,不得使用表情符号。
|
||||||
- **认证状态只显示“已配置/未配置”或掩码值,绝不回显完整 token。**
|
|
||||||
- 本文件只负责显示和输入转发,“测试连接”这类会联网、会连手机的动作
|
|
||||||
放 `settings_ui_event.py`,且必须走后台线程。
|
|
||||||
- 表单必须有**可见标签**,占位符不能当标签用(占位符一输入就没了,
|
|
||||||
用户会忘记这个框是干嘛的)。
|
|
||||||
- 用可滚动布局和 Fluent 设置卡片;MVP 采用显式“保存设置”按钮,
|
|
||||||
不要在同一页混用即时保存。
|
|
||||||
- 校验失败时保留用户已输入的内容,并把焦点移到第一个出错的字段,
|
|
||||||
不要清空重来。
|
|
||||||
- 图标用 `FluentIcon`,**不得用表情符号**。
|
|
||||||
- 页面 `objectName` 固定为 `settingsPage`,不要改。
|
|
||||||
|
|
||||||
注意:设备连接**不是**独立的顶级页面,统一放在本页,
|
|
||||||
见 `docs/client/01-requirements.md` §3。
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt, pyqtSignal
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QAbstractItemView,
|
||||||
|
QFormLayout,
|
||||||
|
QHeaderView,
|
||||||
|
QHBoxLayout,
|
||||||
|
QSizePolicy,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import (
|
||||||
CaptionLabel,
|
CaptionLabel,
|
||||||
CardWidget,
|
CardWidget,
|
||||||
FluentIcon as FIF,
|
FluentIcon as FIF,
|
||||||
LineEdit,
|
LineEdit,
|
||||||
PushButton,
|
PushButton,
|
||||||
|
ScrollArea,
|
||||||
SubtitleLabel,
|
SubtitleLabel,
|
||||||
|
TableView,
|
||||||
TitleLabel,
|
TitleLabel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DevicePage(QWidget):
|
@dataclass(frozen=True)
|
||||||
"""Android 设备连接页。"""
|
class AndroidDeviceRow:
|
||||||
|
"""一台 ADB 已发现设备的界面数据。"""
|
||||||
|
|
||||||
|
serial: str
|
||||||
|
connection_type: str
|
||||||
|
model: str = "—"
|
||||||
|
android_version: str = "—"
|
||||||
|
status: str = "device"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_text(self) -> str:
|
||||||
|
return {
|
||||||
|
"device": "已连接",
|
||||||
|
"offline": "离线",
|
||||||
|
"unauthorized": "未授权",
|
||||||
|
}.get(self.status, self.status or "未知")
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidDeviceTableModel(QAbstractTableModel):
|
||||||
|
"""Android 设备表格模型,使用序列号维护互斥勾选。"""
|
||||||
|
|
||||||
|
checkedDeviceChanged = pyqtSignal(str)
|
||||||
|
|
||||||
|
HEADERS = ("选择", "设备号", "连接方式", "型号", "Android 版本", "状态")
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setObjectName("devicePage")
|
self._rows: list[AndroidDeviceRow] = []
|
||||||
|
self._checked_serial = ""
|
||||||
|
|
||||||
self.addressInput = LineEdit(self)
|
def rowCount(self, parent=QModelIndex()) -> int:
|
||||||
self.addressInput.setText("192.168.0.173:5555")
|
return 0 if parent.isValid() else len(self._rows)
|
||||||
self.addressInput.setPlaceholderText("IP:端口")
|
|
||||||
self.addressInput.setClearButtonEnabled(True)
|
|
||||||
self.addressInput.setAccessibleName("Android 设备地址")
|
|
||||||
|
|
||||||
self.connectButton = PushButton(FIF.LINK, "连接设备", self)
|
def columnCount(self, parent=QModelIndex()) -> int:
|
||||||
self.connectButton.setAccessibleName("连接 Android 设备")
|
return 0 if parent.isValid() else len(self.HEADERS)
|
||||||
self.deviceStatusLabel = CaptionLabel("当前状态:未连接", self)
|
|
||||||
|
|
||||||
card = CardWidget(self)
|
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||||
cardLayout = QVBoxLayout(card)
|
return self.HEADERS[section]
|
||||||
cardLayout.setContentsMargins(24, 20, 24, 22)
|
return super().headerData(section, orientation, role)
|
||||||
cardLayout.setSpacing(12)
|
|
||||||
cardLayout.addWidget(SubtitleLabel("无线调试设备", card))
|
|
||||||
cardLayout.addWidget(CaptionLabel("设备地址", card))
|
|
||||||
|
|
||||||
connectionLayout = QHBoxLayout()
|
def data(self, index, role=Qt.DisplayRole):
|
||||||
connectionLayout.setSpacing(12)
|
if not index.isValid() or not 0 <= index.row() < len(self._rows):
|
||||||
connectionLayout.addWidget(self.addressInput, 1)
|
return None
|
||||||
connectionLayout.addWidget(self.connectButton)
|
|
||||||
cardLayout.addLayout(connectionLayout)
|
|
||||||
cardLayout.addWidget(self.deviceStatusLabel)
|
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
device = self._rows[index.row()]
|
||||||
layout.setContentsMargins(36, 30, 36, 36)
|
if index.column() == 0 and role == Qt.CheckStateRole:
|
||||||
layout.setSpacing(16)
|
return Qt.Checked if device.serial == self._checked_serial else Qt.Unchecked
|
||||||
layout.addWidget(TitleLabel("设备管理", self))
|
|
||||||
# description = BodyLabel("管理 uiautomator2 使用的 Android 设备连接。", self)
|
if role == Qt.DisplayRole:
|
||||||
# description.setWordWrap(True)
|
values = (
|
||||||
# layout.addWidget(description)
|
"",
|
||||||
layout.addWidget(card)
|
device.serial,
|
||||||
layout.addStretch(1)
|
device.connection_type,
|
||||||
|
device.model,
|
||||||
|
device.android_version,
|
||||||
|
device.status_text,
|
||||||
|
)
|
||||||
|
return values[index.column()]
|
||||||
|
|
||||||
|
if role == Qt.TextAlignmentRole:
|
||||||
|
if index.column() in (0, 2, 4, 5):
|
||||||
|
return int(Qt.AlignCenter)
|
||||||
|
return int(Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
|
||||||
|
if role == Qt.ToolTipRole:
|
||||||
|
if index.column() == 0 and device.status != "device":
|
||||||
|
return "设备状态正常后才能选择"
|
||||||
|
if index.column() == 1:
|
||||||
|
return device.serial
|
||||||
|
return None
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
if not index.isValid():
|
||||||
|
return Qt.NoItemFlags
|
||||||
|
|
||||||
|
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||||
|
if index.column() == 0 and self._rows[index.row()].status == "device":
|
||||||
|
flags |= Qt.ItemIsUserCheckable
|
||||||
|
return flags
|
||||||
|
|
||||||
|
def setData(self, index, value, role=Qt.EditRole) -> bool:
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or index.column() != 0
|
||||||
|
or role != Qt.CheckStateRole
|
||||||
|
or self._rows[index.row()].status != "device"
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
serial = self._rows[index.row()].serial
|
||||||
|
checked_serial = serial if value == Qt.Checked else ""
|
||||||
|
self.set_checked_serial(checked_serial)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_devices(self, devices: Iterable[AndroidDeviceRow]) -> None:
|
||||||
|
"""替换设备列表,并按稳定序列号保留有效勾选。"""
|
||||||
|
|
||||||
|
rows = list(devices)
|
||||||
|
valid_serials = {row.serial for row in rows if row.status == "device"}
|
||||||
|
checked_serial = self._checked_serial
|
||||||
|
|
||||||
|
self.beginResetModel()
|
||||||
|
self._rows = rows
|
||||||
|
if checked_serial not in valid_serials:
|
||||||
|
self._checked_serial = ""
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
if checked_serial != self._checked_serial:
|
||||||
|
self.checkedDeviceChanged.emit(self._checked_serial)
|
||||||
|
|
||||||
|
def set_checked_serial(self, serial: str) -> None:
|
||||||
|
"""勾选一台正常设备;传空字符串清除勾选。"""
|
||||||
|
|
||||||
|
if serial:
|
||||||
|
selectable = any(
|
||||||
|
row.serial == serial and row.status == "device" for row in self._rows
|
||||||
|
)
|
||||||
|
if not selectable:
|
||||||
|
serial = ""
|
||||||
|
|
||||||
|
if serial == self._checked_serial:
|
||||||
|
return
|
||||||
|
|
||||||
|
old_serial = self._checked_serial
|
||||||
|
self._checked_serial = serial
|
||||||
|
for candidate in (old_serial, serial):
|
||||||
|
row = self.row_for_serial(candidate)
|
||||||
|
if row >= 0:
|
||||||
|
index = self.index(row, 0)
|
||||||
|
self.dataChanged.emit(index, index, [Qt.CheckStateRole])
|
||||||
|
self.checkedDeviceChanged.emit(serial)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def checked_serial(self) -> str:
|
||||||
|
return self._checked_serial
|
||||||
|
|
||||||
|
def row_for_serial(self, serial: str) -> int:
|
||||||
|
for row, device in enumerate(self._rows):
|
||||||
|
if device.serial == serial:
|
||||||
|
return row
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def device_for_serial(self, serial: str) -> Optional[AndroidDeviceRow]:
|
||||||
|
row = self.row_for_serial(serial)
|
||||||
|
return self._rows[row] if row >= 0 else None
|
||||||
|
|
||||||
|
|
||||||
class SettingsPage(QWidget):
|
class SettingsPage(QWidget):
|
||||||
"""后续设置项的扩展入口。"""
|
"""设备管理与后续应用设置的统一页面。"""
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setObjectName("settingsPage")
|
self.setObjectName("settingsPage")
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
card = CardWidget(self)
|
# 延迟到控件创建完成后绑定,避免事件层在构造过程中访问半成品页面。
|
||||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
from .settings_ui_event import SettingsPageEventBinder
|
||||||
cardLayout = QVBoxLayout(card)
|
|
||||||
cardLayout.setContentsMargins(24, 20, 24, 22)
|
self.eventBinder = SettingsPageEventBinder(self)
|
||||||
cardLayout.setSpacing(8)
|
|
||||||
cardLayout.addWidget(SubtitleLabel("应用设置", card))
|
def _build_ui(self) -> None:
|
||||||
# hint = BodyLabel("主题、日志和任务策略可以在这里继续扩展。", card)
|
self.deviceIdInput = LineEdit(self)
|
||||||
# hint.setWordWrap(True)
|
self.deviceIdInput.setText("待生成")
|
||||||
# cardLayout.addWidget(hint)
|
self.deviceIdInput.setReadOnly(True)
|
||||||
|
self.deviceIdInput.setAccessibleName("当前客户端设备号")
|
||||||
|
|
||||||
|
self.deviceNameInput = LineEdit(self)
|
||||||
|
self.deviceNameInput.setPlaceholderText("请输入便于识别的设备名")
|
||||||
|
self.deviceNameInput.setClearButtonEnabled(True)
|
||||||
|
self.deviceNameInput.setMaxLength(50)
|
||||||
|
self.deviceNameInput.setAccessibleName("当前客户端设备名")
|
||||||
|
|
||||||
|
self.currentDeviceCard = self._build_current_device_card()
|
||||||
|
|
||||||
|
self.addressInput = LineEdit(self)
|
||||||
|
self.addressInput.setPlaceholderText("例如 192.168.0.173:5555")
|
||||||
|
self.addressInput.setClearButtonEnabled(True)
|
||||||
|
self.addressInput.setAccessibleName("Android 无线调试地址")
|
||||||
|
|
||||||
|
self.searchButton = PushButton(FIF.SEARCH, "搜索", self)
|
||||||
|
self.connectButton = PushButton(FIF.LINK, "连接", self)
|
||||||
|
self.saveButton = PushButton(FIF.SAVE, "保存", self)
|
||||||
|
self.deleteButton = PushButton(FIF.DELETE, "删除", self)
|
||||||
|
self.saveButton.setEnabled(False)
|
||||||
|
self.deleteButton.setEnabled(False)
|
||||||
|
|
||||||
|
self.deviceTableModel = AndroidDeviceTableModel(self)
|
||||||
|
self.deviceTable = TableView(self)
|
||||||
|
self.deviceTable.setModel(self.deviceTableModel)
|
||||||
|
self.deviceTable.setAccessibleName("已连接的 Android 设备")
|
||||||
|
self.deviceTable.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.deviceTable.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self.deviceTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||||
|
self.deviceTable.verticalHeader().hide()
|
||||||
|
self.deviceTable.verticalHeader().setDefaultSectionSize(42)
|
||||||
|
self.deviceTable.setMinimumHeight(250)
|
||||||
|
self.deviceTable.setColumnWidth(0, 64)
|
||||||
|
self.deviceTable.setColumnWidth(1, 210)
|
||||||
|
self.deviceTable.setColumnWidth(2, 96)
|
||||||
|
self.deviceTable.setColumnWidth(4, 112)
|
||||||
|
header = self.deviceTable.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.Fixed)
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.Interactive)
|
||||||
|
header.setSectionResizeMode(2, QHeaderView.Fixed)
|
||||||
|
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(4, QHeaderView.Fixed)
|
||||||
|
header.setSectionResizeMode(5, QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
|
self.deviceStatusLabel = CaptionLabel("当前使用设备:未选择", self)
|
||||||
|
self.androidDeviceCard = self._build_android_device_card()
|
||||||
|
|
||||||
|
content = QWidget(self)
|
||||||
|
content.setObjectName("settingsContent")
|
||||||
|
contentLayout = QVBoxLayout(content)
|
||||||
|
contentLayout.setContentsMargins(36, 30, 36, 36)
|
||||||
|
contentLayout.setSpacing(16)
|
||||||
|
contentLayout.addWidget(TitleLabel("设置", content))
|
||||||
|
contentLayout.addWidget(self.currentDeviceCard)
|
||||||
|
contentLayout.addWidget(self.androidDeviceCard)
|
||||||
|
contentLayout.addStretch(1)
|
||||||
|
|
||||||
|
scrollArea = ScrollArea(self)
|
||||||
|
scrollArea.setWidgetResizable(True)
|
||||||
|
scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
scrollArea.setWidget(content)
|
||||||
|
scrollArea.enableTransparentBackground()
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(36, 30, 36, 36)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.setSpacing(16)
|
layout.addWidget(scrollArea)
|
||||||
layout.addWidget(TitleLabel("设置", self))
|
|
||||||
layout.addWidget(card)
|
def _build_current_device_card(self) -> CardWidget:
|
||||||
layout.addStretch(1)
|
card = CardWidget(self)
|
||||||
|
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
|
layout = QVBoxLayout(card)
|
||||||
|
layout.setContentsMargins(24, 20, 24, 22)
|
||||||
|
layout.setSpacing(12)
|
||||||
|
layout.addWidget(SubtitleLabel("当前设备", card))
|
||||||
|
|
||||||
|
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)
|
||||||
|
return card
|
||||||
|
|
||||||
|
def _build_android_device_card(self) -> CardWidget:
|
||||||
|
card = CardWidget(self)
|
||||||
|
layout = QVBoxLayout(card)
|
||||||
|
layout.setContentsMargins(24, 20, 24, 22)
|
||||||
|
layout.setSpacing(12)
|
||||||
|
layout.addWidget(SubtitleLabel("Android 设备", card))
|
||||||
|
|
||||||
|
addressLayout = QHBoxLayout()
|
||||||
|
addressLayout.setSpacing(12)
|
||||||
|
addressLabel = CaptionLabel("无线地址", card)
|
||||||
|
addressLabel.setBuddy(self.addressInput)
|
||||||
|
addressLayout.addWidget(addressLabel)
|
||||||
|
addressLayout.addWidget(self.addressInput, 1)
|
||||||
|
addressLayout.addWidget(self.connectButton)
|
||||||
|
layout.addLayout(addressLayout)
|
||||||
|
|
||||||
|
commandLayout = QHBoxLayout()
|
||||||
|
commandLayout.setSpacing(10)
|
||||||
|
commandLayout.addWidget(self.searchButton)
|
||||||
|
commandLayout.addStretch(1)
|
||||||
|
commandLayout.addWidget(self.saveButton)
|
||||||
|
commandLayout.addWidget(self.deleteButton)
|
||||||
|
layout.addLayout(commandLayout)
|
||||||
|
layout.addWidget(self.deviceTable, 1)
|
||||||
|
layout.addWidget(self.deviceStatusLabel)
|
||||||
|
return card
|
||||||
|
|
||||||
|
def set_client_info(self, device_id: str, device_name: str) -> None:
|
||||||
|
"""显示后续设备身份服务提供的当前客户端信息。"""
|
||||||
|
|
||||||
|
self.deviceIdInput.setText(device_id or "待生成")
|
||||||
|
self.deviceNameInput.setText(device_name)
|
||||||
|
|
||||||
|
def set_android_devices(self, devices: Iterable[AndroidDeviceRow]) -> None:
|
||||||
|
"""显示后续 ADB 服务返回的设备列表。"""
|
||||||
|
|
||||||
|
self.deviceTableModel.set_devices(devices)
|
||||||
|
|
||||||
|
def set_saved_android_device(self, serial: str) -> None:
|
||||||
|
"""显示已经保存并实际用于自动化的 Android 设备。"""
|
||||||
|
|
||||||
|
self.deviceTableModel.set_checked_serial(serial)
|
||||||
|
text = serial if serial else "未选择"
|
||||||
|
self.deviceStatusLabel.setText(f"当前使用设备:{text}")
|
||||||
|
|||||||
@@ -1,25 +1,92 @@
|
|||||||
"""设置页的事件绑定。
|
"""设置页的事件绑定。
|
||||||
|
|
||||||
处理保存设置、测试 Admin 连接、测试设备连接等动作。
|
本文件把界面操作转换为轻量信号,不直接执行 ADB、SQLite 或硬件信息读取。
|
||||||
|
后续应用服务应在后台线程处理阻塞操作,再通过页面更新方法返回结果。
|
||||||
改动本文件前必读 `client/AGENTS.md`。以下几条最容易踩:
|
|
||||||
|
|
||||||
- **凭据(token、密码)不写进 SQLite 的 `app_settings`,也不写进日志。**
|
|
||||||
存到 Windows 凭据管理器,见 `docs/client/06-quality-security.md` §7。
|
|
||||||
`data/` 目录是明文的、设计上就是整个拷走的,拷一次等于泄露一次。
|
|
||||||
- “测试连接”会联网、会连手机,**必须走后台线程**,
|
|
||||||
用 `QObject` + `moveToThread`(模板见 `docs/client/02-architecture.md` §5.1)。
|
|
||||||
在主线程里直接连,界面会卡住好几秒。
|
|
||||||
- 非敏感设置存 `app_settings` 表,键名沿用 `<分组>.<名称>` 的写法,
|
|
||||||
清单见 `docs/client/03-data-model.md` §6。
|
|
||||||
- 涉及路径的设置(日志目录、Artifact 目录)一律基于 `data_dir()`,
|
|
||||||
**不要硬编码,也不要用 `os.getcwd()`**——打包成 exe 后会失效,
|
|
||||||
见 `docs/client/03-data-model.md` §2.2。
|
|
||||||
- 保存前要校验:数量上限、价格偏差、轮询周期这些填错了会直接影响
|
|
||||||
采购安全,不能存进去再说。
|
|
||||||
- 测试结果用 `InfoBar` 反馈,失败时说清“哪一步失败、下一步做什么”,
|
|
||||||
不要把原始异常堆栈贴到界面上。
|
|
||||||
|
|
||||||
注意:真实下单开关属于红线,默认关闭,不得在这里做成
|
|
||||||
一个随手能打开的普通开关,见根目录 `AGENTS.md` 红线第 2 条。
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsPageEventBinder(QObject):
|
||||||
|
"""绑定设备管理控件,并向应用层发出稳定事件。"""
|
||||||
|
|
||||||
|
searchRequested = pyqtSignal()
|
||||||
|
connectRequested = pyqtSignal(str)
|
||||||
|
saveRequested = pyqtSignal(str, str)
|
||||||
|
deleteRequested = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, page):
|
||||||
|
super().__init__(page)
|
||||||
|
self._page = page
|
||||||
|
self._busy = False
|
||||||
|
|
||||||
|
page.searchButton.clicked.connect(self._request_search)
|
||||||
|
page.connectButton.clicked.connect(self._request_connect)
|
||||||
|
page.saveButton.clicked.connect(self._request_save)
|
||||||
|
page.deleteButton.clicked.connect(self._request_delete)
|
||||||
|
page.addressInput.textChanged.connect(self._sync_button_state)
|
||||||
|
page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state)
|
||||||
|
self._sync_button_state()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _request_search(self) -> None:
|
||||||
|
if self._busy:
|
||||||
|
return
|
||||||
|
self.searchRequested.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _request_connect(self) -> None:
|
||||||
|
if self._busy:
|
||||||
|
return
|
||||||
|
|
||||||
|
address = self._page.addressInput.text().strip()
|
||||||
|
if not address:
|
||||||
|
self._page.deviceStatusLabel.setText(
|
||||||
|
"连接失败:请先填写无线调试地址,例如 192.168.0.173:5555"
|
||||||
|
)
|
||||||
|
self._page.addressInput.setFocus()
|
||||||
|
return
|
||||||
|
self.connectRequested.emit(address)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _request_save(self) -> None:
|
||||||
|
if self._busy:
|
||||||
|
return
|
||||||
|
|
||||||
|
serial = self._page.deviceTableModel.checked_serial
|
||||||
|
if not serial:
|
||||||
|
self._page.deviceStatusLabel.setText("保存失败:请先勾选一台已连接设备")
|
||||||
|
self._page.deviceTable.setFocus()
|
||||||
|
return
|
||||||
|
|
||||||
|
device_name = self._page.deviceNameInput.text().strip()
|
||||||
|
self.saveRequested.emit(device_name, serial)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _request_delete(self) -> None:
|
||||||
|
if self._busy:
|
||||||
|
return
|
||||||
|
|
||||||
|
serial = self._page.deviceTableModel.checked_serial
|
||||||
|
if not serial:
|
||||||
|
self._page.deviceStatusLabel.setText("删除失败:请先勾选一台设备")
|
||||||
|
self._page.deviceTable.setFocus()
|
||||||
|
return
|
||||||
|
self.deleteRequested.emit(serial)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _sync_button_state(self, *_args) -> None:
|
||||||
|
serial = self._page.deviceTableModel.checked_serial
|
||||||
|
has_address = bool(self._page.addressInput.text().strip())
|
||||||
|
self._page.searchButton.setEnabled(not self._busy)
|
||||||
|
self._page.connectButton.setEnabled(not self._busy and has_address)
|
||||||
|
self._page.saveButton.setEnabled(not self._busy and bool(serial))
|
||||||
|
self._page.deleteButton.setEnabled(not self._busy and bool(serial))
|
||||||
|
|
||||||
|
def set_busy(self, busy: bool, message: str = "") -> None:
|
||||||
|
"""切换界面忙碌状态,防止用户重复提交设备命令。"""
|
||||||
|
|
||||||
|
self._busy = busy
|
||||||
|
self._sync_button_state()
|
||||||
|
if message:
|
||||||
|
self._page.deviceStatusLabel.setText(message)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from qfluentwidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .pdd_ui import PDDTaskPage
|
from .pdd_ui import PDDTaskPage
|
||||||
from .settings_ui import DevicePage, SettingsPage
|
from .settings_ui import SettingsPage
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(FluentWindow):
|
class MainWindow(FluentWindow):
|
||||||
@@ -38,11 +38,9 @@ class MainWindow(FluentWindow):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.pddTaskPage = PDDTaskPage(self)
|
self.pddTaskPage = PDDTaskPage(self)
|
||||||
self.devicePage = DevicePage(self)
|
|
||||||
self.settingsPage = SettingsPage(self)
|
self.settingsPage = SettingsPage(self)
|
||||||
|
|
||||||
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
|
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
|
||||||
self.addSubInterface(self.devicePage, FIF.IOT, "设备")
|
|
||||||
self.addSubInterface(
|
self.addSubInterface(
|
||||||
self.settingsPage,
|
self.settingsPage,
|
||||||
FIF.SETTING,
|
FIF.SETTING,
|
||||||
|
|||||||
Reference in New Issue
Block a user