Files
cmautobuy/client/src/settings_ui.py
T

414 lines
15 KiB
Python
Raw Normal View History

"""设置页的界面结构。
本文件只负责显示设备信息和提供界面更新入口。按钮事件统一由
``settings_ui_event.py`` 绑定;ADB、SQLite 和硬件信息读取不得放在这里。
改动本文件前必读 ``client/AGENTS.md``。页面 ``objectName`` 固定为
``settingsPage``,图标使用 ``FluentIcon``,不得使用表情符号。
"""
2026-08-06 12:06:16 +08:00
from dataclasses import dataclass
from typing import Iterable, Optional
from PyQt5.QtCore import QAbstractTableModel, QEvent, QModelIndex, Qt, pyqtSignal
from PyQt5.QtWidgets import (
QAbstractItemView,
QFormLayout,
QHeaderView,
QHBoxLayout,
QSizePolicy,
QVBoxLayout,
QWidget,
)
2026-08-06 12:06:16 +08:00
from qfluentwidgets import (
CaptionLabel,
CardWidget,
FluentIcon as FIF,
LineEdit,
PushButton,
ScrollArea,
2026-08-06 12:06:16 +08:00
SubtitleLabel,
TableView,
2026-08-06 12:06:16 +08:00
TitleLabel,
)
@dataclass(frozen=True)
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 版本", "状态")
2026-08-06 12:06:16 +08:00
def __init__(self, parent=None):
super().__init__(parent)
self._rows: list[AndroidDeviceRow] = []
self._checked_serial = ""
self._placeholder_row_count = 1
2026-08-06 12:06:16 +08:00
def rowCount(self, parent=QModelIndex()) -> int:
if parent.isValid():
return 0
return len(self._rows) or self._placeholder_row_count
2026-08-06 12:06:16 +08:00
def columnCount(self, parent=QModelIndex()) -> int:
return 0 if parent.isValid() else len(self.HEADERS)
2026-08-06 12:06:16 +08:00
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.HEADERS[section]
return super().headerData(section, orientation, role)
2026-08-06 12:06:16 +08:00
def data(self, index, role=Qt.DisplayRole):
if not index.isValid() or not 0 <= index.row() < len(self._rows):
return None
2026-08-06 12:06:16 +08:00
device = self._rows[index.row()]
if index.column() == 0 and role == Qt.CheckStateRole:
return Qt.Checked if device.serial == self._checked_serial else Qt.Unchecked
if role == Qt.DisplayRole:
values = (
"",
device.serial,
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() or index.row() >= len(self._rows):
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.row() >= len(self._rows)
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_placeholder_row_count(self, count: int) -> None:
"""设置空设备列表占位行数,真实设备行不受影响。"""
count = max(1, count)
if self._rows or count == self._placeholder_row_count:
self._placeholder_row_count = count
return
self.beginResetModel()
self._placeholder_row_count = count
self.endResetModel()
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
2026-08-06 12:06:16 +08:00
class SettingsPage(QWidget):
"""设备管理与后续应用设置的统一页面。"""
2026-08-06 12:06:16 +08:00
2026-08-06 17:57:49 +08:00
def __init__(
self,
parent=None,
settings_repository=None,
admin_gateway=None,
2026-08-07 11:53:03 +08:00
android_device_service=None,
2026-08-06 17:57:49 +08:00
):
2026-08-06 12:06:16 +08:00
super().__init__(parent)
self.setObjectName("settingsPage")
self._build_ui()
2026-08-06 12:06:16 +08:00
# 延迟到控件创建完成后绑定,避免事件层在构造过程中访问半成品页面。
from .settings_ui_event import SettingsPageEventBinder
2026-08-06 17:57:49 +08:00
self.eventBinder = SettingsPageEventBinder(
self,
settings_repository=settings_repository,
admin_gateway=admin_gateway,
2026-08-07 11:53:03 +08:00
android_device_service=android_device_service,
2026-08-06 17:57:49 +08:00
)
def _build_ui(self) -> None:
self.deviceIdInput = LineEdit(self)
self.deviceIdInput.setText("待生成")
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.currentDeviceSaveButton = PushButton(FIF.SAVE, "保存", self)
self.currentDeviceSaveButton.setAccessibleName("保存当前设备信息")
2026-08-06 17:57:49 +08:00
self.currentDeviceStatusLabel = CaptionLabel(
"设备信息尚未保存", self
)
self.currentDeviceStatusLabel.setAccessibleName("当前设备保存状态")
self.currentDeviceStatusLabel.setWordWrap(True)
self.currentDeviceCard = self._build_current_device_card()
self.searchButton = PushButton(FIF.SEARCH, "搜索", self)
2026-08-07 14:42:00 +08:00
self.convertWifiButton = PushButton(FIF.WIFI, "转为 Wi-Fi", self)
self.convertWifiButton.setAccessibleName(
"将勾选的 USB Android 设备转为 Wi-Fi 连接"
)
self.convertWifiButton.setToolTip(
"开启 ADB 5555,并在拔掉 USB 后重新连接"
)
self.saveButton = PushButton(FIF.SAVE, "保存", self)
self.deleteButton = PushButton(FIF.DELETE, "删除", self)
2026-08-07 14:42:00 +08:00
self.convertWifiButton.setEnabled(False)
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)
self.deviceTable.viewport().installEventFilter(self)
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)
2026-08-07 11:53:03 +08:00
self.deviceStatusLabel.setAccessibleName("Android 设备搜索和选择状态")
self.deviceStatusLabel.setWordWrap(True)
self.pddAppStatusLabel = CaptionLabel("未检测", self)
self.pddAppStatusLabel.setAccessibleName("PDD 应用安装状态")
self.pddAppStatusLabel.setWordWrap(True)
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()
2026-08-06 12:06:16 +08:00
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(scrollArea)
def eventFilter(self, watched, event):
if watched is self.deviceTable.viewport() and event.type() == QEvent.Resize:
self._update_device_placeholder_rows()
return super().eventFilter(watched, event)
def _update_device_placeholder_rows(self) -> None:
"""用不可交互空行填满 Android 设备表格的可视区域。"""
row_height = max(1, self.deviceTable.verticalHeader().defaultSectionSize())
viewport_height = max(row_height, self.deviceTable.viewport().height())
row_count = max(1, viewport_height // row_height)
self.deviceTableModel.set_placeholder_row_count(row_count)
def _build_current_device_card(self) -> CardWidget:
card = CardWidget(self)
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
layout = QVBoxLayout(card)
layout.setContentsMargins(24, 20, 24, 22)
layout.setSpacing(12)
titleLayout = QHBoxLayout()
titleLayout.setSpacing(12)
titleLayout.addWidget(SubtitleLabel("当前设备", card))
titleLayout.addStretch(1)
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)
commandLayout = QHBoxLayout()
commandLayout.addStretch(1)
commandLayout.addWidget(self.currentDeviceSaveButton)
layout.addLayout(commandLayout)
return card
def _build_android_device_card(self) -> CardWidget:
card = CardWidget(self)
layout = QVBoxLayout(card)
layout.setContentsMargins(24, 20, 24, 22)
layout.setSpacing(12)
titleLayout = QHBoxLayout()
titleLayout.setSpacing(12)
titleLayout.addWidget(SubtitleLabel("Android 设备", card))
titleLayout.addStretch(1)
titleLayout.addWidget(self.deviceStatusLabel, 1)
layout.addLayout(titleLayout)
pddStatusLayout = QHBoxLayout()
pddStatusLayout.setSpacing(12)
pddStatusLayout.addWidget(CaptionLabel("PDD 应用", card))
pddStatusLayout.addWidget(self.pddAppStatusLabel, 1)
layout.addLayout(pddStatusLayout)
commandLayout = QHBoxLayout()
commandLayout.setSpacing(10)
commandLayout.addWidget(self.searchButton)
2026-08-07 14:42:00 +08:00
commandLayout.addWidget(self.convertWifiButton)
commandLayout.addStretch(1)
commandLayout.addWidget(self.saveButton)
commandLayout.addWidget(self.deleteButton)
layout.addLayout(commandLayout)
layout.addWidget(self.deviceTable, 1)
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)
2026-08-06 17:57:49 +08:00
def set_current_device_status(self, message: str) -> None:
"""显示本地保存和 Admin 登记两个阶段的结果。"""
self.currentDeviceStatusLabel.setText(message)
def set_android_devices(self, devices: Iterable[AndroidDeviceRow]) -> None:
"""显示后续 ADB 服务返回的设备列表。"""
self.deviceTableModel.set_devices(devices)
self._update_device_placeholder_rows()
def set_pdd_app_status(self, message: str) -> None:
"""显示当前勾选设备上的 PDD 安装检测结果。"""
self.pddAppStatusLabel.setText(message)
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}")